Add support for rendering APNGs.
This commit is contained in:
parent
1d2ffe56fb
commit
250402e9b9
44 changed files with 2822 additions and 108 deletions
58
app/src/main/java/org/signal/glide/Log.java
Normal file
58
app/src/main/java/org/signal/glide/Log.java
Normal file
|
@ -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) { }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
18
app/src/main/java/org/signal/glide/SignalGlideCodecs.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
52
app/src/main/java/org/signal/glide/apng/APNGDrawable.java
Normal file
|
@ -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<APNGDecoder> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
211
app/src/main/java/org/signal/glide/apng/decode/APNGDecoder.java
Normal file
|
@ -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<APNGReader, APNGWriter> {
|
||||||
|
|
||||||
|
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<Chunk> chunks = APNGParser.parse(reader);
|
||||||
|
List<Chunk> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
147
app/src/main/java/org/signal/glide/apng/decode/APNGFrame.java
Normal file
|
@ -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<APNGReader, APNGWriter> {
|
||||||
|
public final byte blend_op;
|
||||||
|
public final byte dispose_op;
|
||||||
|
byte[] ihdrData;
|
||||||
|
List<Chunk> imageChunks = new ArrayList<>();
|
||||||
|
List<Chunk> 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<CRC32> 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;
|
||||||
|
}
|
||||||
|
}
|
145
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
145
app/src/main/java/org/signal/glide/apng/decode/APNGParser.java
Normal file
|
@ -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<Chunk> parse(APNGReader reader) throws IOException {
|
||||||
|
if (!reader.matchFourCC("\u0089PNG") || !reader.matchFourCC("\r\n\u001a\n")) {
|
||||||
|
throw new FormatException();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Chunk> 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;
|
||||||
|
}
|
||||||
|
}
|
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
53
app/src/main/java/org/signal/glide/apng/decode/Chunk.java
Normal file
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
121
app/src/main/java/org/signal/glide/apng/decode/FCTLChunk.java
Normal file
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<code> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
|
@ -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:
|
||||||
|
* <p>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<APNGReader, APNGWriter> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
74
app/src/main/java/org/signal/glide/apng/io/APNGReader.java
Normal file
|
@ -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<byte[]> __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;
|
||||||
|
}
|
||||||
|
}
|
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
41
app/src/main/java/org/signal/glide/apng/io/APNGWriter.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Decoder extends FrameSeqDecoder> 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<AnimationCallback> 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();
|
||||||
|
}
|
||||||
|
}
|
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
33
app/src/main/java/org/signal/glide/common/decode/Frame.java
Normal file
|
@ -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<R extends Reader, W extends Writer> {
|
||||||
|
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);
|
||||||
|
}
|
|
@ -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<R extends Reader, W extends Writer> {
|
||||||
|
private static final String TAG = FrameSeqDecoder.class.getSimpleName();
|
||||||
|
private final int taskId;
|
||||||
|
|
||||||
|
private final Loader mLoader;
|
||||||
|
private final Handler workerHandler;
|
||||||
|
protected List<Frame> frames = new ArrayList<>();
|
||||||
|
protected int frameIndex = -1;
|
||||||
|
private int playCount;
|
||||||
|
private Integer loopLimit = null;
|
||||||
|
private Set<RenderListener> 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<Bitmap> cacheBitmaps = new HashSet<>();
|
||||||
|
protected Map<Bitmap, Canvas> 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<Bitmap> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<HandlerThread> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
30
app/src/main/java/org/signal/glide/common/io/FileReader.java
Normal file
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
35
app/src/main/java/org/signal/glide/common/io/Reader.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
29
app/src/main/java/org/signal/glide/common/io/Writer.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
19
app/src/main/java/org/signal/glide/common/loader/Loader.java
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||||
|
|
||||||
import org.conscrypt.Conscrypt;
|
import org.conscrypt.Conscrypt;
|
||||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||||
|
import org.signal.glide.SignalGlideCodecs;
|
||||||
import org.signal.ringrtc.CallManager;
|
import org.signal.ringrtc.CallManager;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
|
@ -127,6 +129,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
|
||||||
initializePendingMessages();
|
initializePendingMessages();
|
||||||
initializeBlobProvider();
|
initializeBlobProvider();
|
||||||
initializeCleanup();
|
initializeCleanup();
|
||||||
|
initializeGlideCodecs();
|
||||||
|
|
||||||
FeatureFlags.init();
|
FeatureFlags.init();
|
||||||
NotificationChannels.create(this);
|
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
|
@Override
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));
|
||||||
|
|
73
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java
vendored
Normal file
73
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngBufferCacheDecoder.java
vendored
Normal file
|
@ -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<ByteBuffer, APNGDecoder> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) {
|
||||||
|
return APNGParser.isAPNG(new ByteBufferReader(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Resource<APNGDecoder> 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<APNGDecoder> {
|
||||||
|
private final APNGDecoder decoder;
|
||||||
|
private final int size;
|
||||||
|
|
||||||
|
FrameSeqDecoderResource(@NonNull APNGDecoder decoder, int size) {
|
||||||
|
this.decoder = decoder;
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull Class<APNGDecoder> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
48
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java
vendored
Normal file
48
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngFrameDrawableTranscoder.java
vendored
Normal file
|
@ -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<APNGDecoder, Drawable> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Resource<Drawable> transcode(@NonNull Resource<APNGDecoder> toTranscode, @NonNull Options options) {
|
||||||
|
APNGDecoder decoder = toTranscode.get();
|
||||||
|
APNGDrawable drawable = new APNGDrawable(decoder);
|
||||||
|
|
||||||
|
drawable.setAutoPlay(false);
|
||||||
|
drawable.setLoopLimit(0);
|
||||||
|
|
||||||
|
return new DrawableResource<Drawable>(drawable) {
|
||||||
|
@Override
|
||||||
|
public @NonNull Class<Drawable> getResourceClass() {
|
||||||
|
return Drawable.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void recycle() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java
vendored
Normal file
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/ApngStreamCacheDecoder.java
vendored
Normal file
|
@ -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<InputStream, APNGDecoder> {
|
||||||
|
|
||||||
|
private final ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder;
|
||||||
|
|
||||||
|
public ApngStreamCacheDecoder(ResourceDecoder<ByteBuffer, APNGDecoder> byteBufferDecoder) {
|
||||||
|
this.byteBufferDecoder = byteBufferDecoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||||
|
return APNGParser.isAPNG(new StreamReader(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable Resource<APNGDecoder> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java
vendored
Normal file
51
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedApngCacheEncoder.java
vendored
Normal file
|
@ -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<APNGDecoder> {
|
||||||
|
|
||||||
|
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<APNGDecoder> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<File, Bitmap> {
|
|
||||||
|
|
||||||
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<Bitmap> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,8 +33,6 @@ public class EncryptedBitmapResourceEncoder extends EncryptedCoder implements Re
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
@Override
|
@Override
|
||||||
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
|
public boolean encode(@NonNull Resource<Bitmap> data, @NonNull File file, @NonNull Options options) {
|
||||||
Log.i(TAG, "Encrypted resource encoder running: " + file.toString());
|
|
||||||
|
|
||||||
Bitmap bitmap = data.get();
|
Bitmap bitmap = data.get();
|
||||||
Bitmap.CompressFormat format = getFormat(bitmap, options);
|
Bitmap.CompressFormat format = getFormat(bitmap, options);
|
||||||
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);
|
int quality = options.get(BitmapEncoder.COMPRESSION_QUALITY);
|
||||||
|
|
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java
vendored
Normal file
44
app/src/main/java/org/thoughtcrime/securesms/glide/cache/EncryptedCacheDecoder.java
vendored
Normal file
|
@ -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<DecodeType> extends EncryptedCoder implements ResourceDecoder<File, DecodeType> {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(EncryptedCacheDecoder.class);
|
||||||
|
|
||||||
|
private final byte[] secret;
|
||||||
|
private final ResourceDecoder<InputStream, DecodeType> decoder;
|
||||||
|
|
||||||
|
public EncryptedCacheDecoder(byte[] secret, ResourceDecoder<InputStream, DecodeType> 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<DecodeType> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,8 +30,6 @@ public class EncryptedCacheEncoder extends EncryptedCoder implements Encoder<Inp
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
@Override
|
@Override
|
||||||
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
|
public boolean encode(@NonNull InputStream data, @NonNull File file, @NonNull Options options) {
|
||||||
Log.i(TAG, "Encrypted cache encoder running: " + file.toString());
|
|
||||||
|
|
||||||
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
|
byte[] buffer = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
|
||||||
|
|
||||||
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
|
try (OutputStream outputStream = createEncryptedOutputStream(secret, file)) {
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
package org.thoughtcrime.securesms.glide.cache;
|
|
||||||
|
|
||||||
|
|
||||||
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.gif.GifDrawable;
|
|
||||||
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class EncryptedGifCacheDecoder extends EncryptedCoder implements ResourceDecoder<File, GifDrawable> {
|
|
||||||
|
|
||||||
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<GifDrawable> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.mms;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
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.AttachmentSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
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.ChunkedImageUrlLoader;
|
||||||
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
import org.thoughtcrime.securesms.glide.ContactPhotoLoader;
|
||||||
|
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
|
||||||
import org.thoughtcrime.securesms.glide.OkHttpUrlLoader;
|
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.EncryptedBitmapResourceEncoder;
|
||||||
|
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder;
|
||||||
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
|
||||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifCacheDecoder;
|
|
||||||
import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder;
|
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.AttachmentStreamUriLoader.AttachmentModel;
|
||||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||||
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
|
||||||
|
@ -42,6 +48,7 @@ import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
public class SignalGlideModule extends AppGlideModule {
|
public class SignalGlideModule extends AppGlideModule {
|
||||||
|
@ -63,14 +70,25 @@ public class SignalGlideModule extends AppGlideModule {
|
||||||
byte[] secret = attachmentSecret.getModernKey();
|
byte[] secret = attachmentSecret.getModernKey();
|
||||||
|
|
||||||
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
|
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(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(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(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context));
|
||||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||||
|
|
Loading…
Add table
Reference in a new issue