Move the org.signal.glide code inside signal-android into lib/glide.

This commit is contained in:
Alex Hart
2026-01-29 10:59:15 -04:00
committed by Greyson Parrelli
parent 709adf05aa
commit 7bd3482367
92 changed files with 153 additions and 83 deletions

View 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) {
SignalGlideCodecs.getLogProvider().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) { }
};
}
}

View 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;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide
import android.app.Application
import android.net.Uri
import org.signal.glide.common.io.InputStreamFactory
/**
* Dependencies for the Glide library module, provided by the host application.
*/
object SignalGlideDependencies {
private lateinit var _application: Application
private lateinit var _provider: Provider
@JvmStatic
@Synchronized
fun init(application: Application, provider: Provider) {
if (this::_application.isInitialized || this::_provider.isInitialized) {
return
}
_application = application
_provider = provider
}
val application: Application
get() = _application
fun getUriInputStreamFactory(uri: Uri): InputStreamFactory = _provider.getUriInputStreamFactory(uri)
interface Provider {
/**
* A factory which can create an [java.io.InputStream] from a given [Uri]
*/
fun getUriInputStreamFactory(uri: Uri): InputStreamFactory
}
}

View File

@@ -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.core.util.logging.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 = Log.tag(FrameAnimationDrawable.class);
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();
}
}

View 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);
}

View File

@@ -0,0 +1,541 @@
/*
* 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.core.util.logging.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 org.signal.glide.load.resource.apng.decode.APNGDecoder;
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 = Log.tag(FrameSeqDecoder.class);
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) throws IOException {
fullRect = rect;
int capacity = APNGDecoder.getSafeAllocationSize(fullRect.width(), fullRect.height(), sampleSize);
frameBuffer = ByteBuffer.allocate(capacity);
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;
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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 org.signal.core.util.ThreadUtil;
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, ThreadUtil.PRIORITY_BACKGROUND_THREAD);
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();
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View 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));
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.common.io
import android.app.ActivityManager
import android.content.Context
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
import org.signal.core.util.gibiBytes
import org.signal.core.util.mebiBytes
import org.signal.glide.SignalGlideDependencies
import org.signal.glide.common.io.GlideStreamConfig.MAX_MARK_LIMIT
import org.signal.glide.common.io.GlideStreamConfig.MIN_MARK_LIMIT
object GlideStreamConfig {
private val MIN_MARK_LIMIT: ByteSize = 5.mebiBytes // Glide default
private val MAX_MARK_LIMIT: ByteSize = 8.mebiBytes
private val LOW_MEMORY_THRESHOLD: ByteSize = 4.gibiBytes
private val HIGH_MEMORY_THRESHOLD: ByteSize = 12.gibiBytes
@JvmStatic
val markReadLimitBytes: Int by lazy { calculateScaledMarkLimit(context = SignalGlideDependencies.application).inWholeBytes.toInt() }
/**
* Calculates buffer size, scaling proportionally from [MIN_MARK_LIMIT] to [MAX_MARK_LIMIT] based on how much memory the device has.
*/
private fun calculateScaledMarkLimit(context: Context): ByteSize {
val deviceMemory = getAvailableDeviceMemory(context)
return when {
deviceMemory <= LOW_MEMORY_THRESHOLD -> MIN_MARK_LIMIT
deviceMemory >= HIGH_MEMORY_THRESHOLD -> MAX_MARK_LIMIT
else -> calculateScaledSize(deviceMemory)
}
}
private fun getAvailableDeviceMemory(context: Context): ByteSize {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo().apply {
activityManager.getMemoryInfo(this)
}
return memoryInfo.totalMem.bytes
}
private fun calculateScaledSize(deviceMemory: ByteSize): ByteSize {
val ratio: Float = (deviceMemory - LOW_MEMORY_THRESHOLD).percentageOf(HIGH_MEMORY_THRESHOLD - LOW_MEMORY_THRESHOLD)
val offsetBytes = (ratio * (MAX_MARK_LIMIT.inWholeBytes - MIN_MARK_LIMIT.inWholeBytes)).toLong()
return MIN_MARK_LIMIT + ByteSize(offsetBytes)
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.common.io
import android.net.Uri
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream
import org.signal.core.util.logging.Log
import org.signal.glide.SignalGlideDependencies
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
interface InputStreamFactory {
companion object {
@JvmStatic
fun build(uri: Uri): InputStreamFactory = SignalGlideDependencies.getUriInputStreamFactory(uri)
@JvmStatic
fun build(file: File): InputStreamFactory = FileInputStreamFactory(file)
}
fun create(): InputStream
fun createRecyclable(byteArrayPool: ArrayPool): InputStream = RecyclableBufferedInputStream(create(), byteArrayPool)
}
/**
* A factory that creates a new [InputStream] for the given [File] each time [create] is called.
*/
class FileInputStreamFactory(
private val file: File
) : InputStreamFactory {
companion object {
private val TAG = Log.tag(FileInputStreamFactory::class)
}
override fun create(): InputStream {
return try {
FileInputStream(file)
} catch (e: Exception) {
Log.w(TAG, "Error creating input stream for File.", e)
throw e
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.signal.glide.common.io;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.Synthetic;
import java.io.IOException;
import java.io.InputStream;
/**
* Implementation for {@link InputStream}s that rewinds streams by wrapping them in a buffered stream. This is a copy of
* {@link com.bumptech.glide.load.data.InputStreamRewinder that is modified to use GlideStreamConfig.markReadLimitBytes in place of a hardcoded MARK_READ_LIMIT.
*/
public final class InputStreamRewinder implements DataRewinder<InputStream> {
private final RecyclableBufferedInputStream bufferedStream;
@Synthetic
public InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) {
// We don't check is.markSupported() here because RecyclableBufferedInputStream allows resetting
// after exceeding GlideStreamConfig.markReadLimitBytes, which other InputStreams don't guarantee.
bufferedStream = new RecyclableBufferedInputStream(is, byteArrayPool);
bufferedStream.mark(GlideStreamConfig.getMarkReadLimitBytes());
}
@NonNull
@Override
public InputStream rewindAndGet() throws IOException {
bufferedStream.reset();
return bufferedStream;
}
@Override
public void cleanup() {
bufferedStream.release();
}
public void fixMarkLimits() {
bufferedStream.fixMarkLimit();
}
/**
* Factory for producing {@link InputStreamRewinder}s from {@link InputStream}s.
*/
public static final class Factory implements DataRewinder.Factory<InputStream> {
private final ArrayPool byteArrayPool;
public Factory(ArrayPool byteArrayPool) {
this.byteArrayPool = byteArrayPool;
}
@NonNull
@Override
public DataRewinder<InputStream> build(@NonNull InputStream data) {
return new InputStreamRewinder(data, byteArrayPool);
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
}
}

View 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;
}

View File

@@ -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;
}
}

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View 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;
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.decryptableuri
import android.net.Uri
import com.bumptech.glide.load.Key
import java.security.MessageDigest
data class DecryptableUri(val uri: Uri) : Key {
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(uri.toString().toByteArray())
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.decryptableuri
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import org.signal.glide.common.io.InputStreamFactory
/**
* A Glide [DataFetcher] that retrieves an [InputStreamFactory] for a [DecryptableUri].
*/
class DecryptableUriStreamFetcher(
private val decryptableUri: DecryptableUri
) : DataFetcher<InputStreamFactory> {
override fun getDataClass(): Class<InputStreamFactory> = InputStreamFactory::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStreamFactory>) {
try {
callback.onDataReady(InputStreamFactory.build(decryptableUri.uri))
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}
override fun cancel() = Unit
override fun cleanup() = Unit
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.decryptableuri
import android.content.Context
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import org.signal.glide.common.io.InputStreamFactory
/**
* A Glide [ModelLoader] that handles conversion from [DecryptableUri] to [InputStreamFactory].
*/
class DecryptableUriStreamLoader(
private val context: Context
) : ModelLoader<DecryptableUri, InputStreamFactory> {
override fun handles(model: DecryptableUri): Boolean = true
override fun buildLoadData(
model: DecryptableUri,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStreamFactory> {
val sourceKey = ObjectKey(model)
val dataFetcher = DecryptableUriStreamFetcher(model)
return ModelLoader.LoadData(sourceKey, dataFetcher)
}
class Factory(
private val context: Context
) : ModelLoaderFactory<DecryptableUri, InputStreamFactory> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<DecryptableUri, InputStreamFactory> {
return DecryptableUriStreamLoader(context)
}
override fun teardown() = Unit
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.load
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream
import org.signal.glide.common.io.GlideStreamConfig
import org.signal.glide.common.io.InputStreamFactory
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
typealias GlideImageHeaderParserUtils = com.bumptech.glide.load.ImageHeaderParserUtils
object ImageHeaderParserUtils {
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
if (inputStream == null) {
return ImageHeaderParser.ImageType.UNKNOWN
}
val markableStream = if (!inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
return getType(
parsers = parsers,
getTypeAndRewind = { parser ->
try {
parser.getType(markableStream)
} finally {
markableStream.reset()
}
}
)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, buffer)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getType
*/
@JvmStatic
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
private fun getType(
parsers: List<ImageHeaderParser>,
getTypeAndRewind: (ImageHeaderParser) -> ImageHeaderParser.ImageType
): ImageHeaderParser.ImageType {
return parsers.firstNotNullOfOrNull { parser ->
getTypeAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.ImageType.UNKNOWN }
} ?: ImageHeaderParser.ImageType.UNKNOWN
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
buffer: ByteBuffer,
arrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, buffer, arrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder,
byteArrayPool: ArrayPool
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool,
allowStreamRewind: Boolean
): Int {
if (inputStream == null) {
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
val markableStream = if (allowStreamRewind && !inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
if (allowStreamRewind) {
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
}
return getOrientation(
parsers = parsers,
getOrientationAndRewind = { parser ->
try {
parser.getOrientation(markableStream, byteArrayPool)
} finally {
if (allowStreamRewind) {
markableStream.reset()
}
}
}
)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientationWithFallbacks(
parsers: List<ImageHeaderParser>,
inputStreamFactory: InputStreamFactory,
byteArrayPool: ArrayPool
): Int {
val orientationFromParsers = getOrientation(
parsers = parsers,
inputStream = inputStreamFactory.createRecyclable(byteArrayPool),
byteArrayPool = byteArrayPool,
allowStreamRewind = false
)
if (orientationFromParsers != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromParsers
val orientationFromExif = ExifInterface(inputStreamFactory.createRecyclable(byteArrayPool))
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
if (orientationFromExif != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromExif
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
private fun getOrientation(
parsers: List<ImageHeaderParser>,
getOrientationAndRewind: (ImageHeaderParser) -> Int
): Int {
return parsers.firstNotNullOfOrNull { parser ->
getOrientationAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.UNKNOWN_ORIENTATION }
} ?: ImageHeaderParser.UNKNOWN_ORIENTATION
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.load
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import kotlin.math.max
import kotlin.math.min
object SignalDownsampleStrategy {
/**
* Center outside, but don't up-scale, only downscale. You should be setting centerOutside
* on the target image view to still maintain center outside behavior.
*/
@JvmField
val CENTER_OUTSIDE_NO_UPSCALE: DownsampleStrategy = CenterOutsideNoUpscale()
private class CenterOutsideNoUpscale : DownsampleStrategy() {
override fun getScaleFactor(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): Float {
val widthPercentage = requestedWidth / sourceWidth.toFloat()
val heightPercentage = requestedHeight / sourceHeight.toFloat()
return min(MAX_SCALE_FACTOR, max(widthPercentage, heightPercentage))
}
override fun getSampleSizeRounding(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): SampleSizeRounding {
return SampleSizeRounding.QUALITY
}
companion object {
private const val MAX_SCALE_FACTOR = 1f
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng;
import android.content.Context;
import org.signal.glide.common.FrameAnimationDrawable;
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;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
/**
* @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);
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import org.signal.glide.load.resource.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();
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.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.core.util.logging.Log;
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 org.signal.glide.load.resource.apng.io.APNGReader;
import org.signal.glide.load.resource.apng.io.APNGWriter;
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 = Log.tag(APNGDecoder.class);
private APNGWriter apngWriter;
private int mLoopCount;
private final Paint paint = new Paint();
public static final int MAX_DIMENSION = 4096;
public static final long MAX_TOTAL_PIXELS = 64_000_000L;
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);
}
}
int capacity = getSafeAllocationSize(canvasWidth, canvasHeight, sampleSize);
frameBuffer = ByteBuffer.allocate(capacity);
snapShot.byteBuffer = ByteBuffer.allocate(capacity);
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);
}
}
public static int getSafeAllocationSize(int width, int height, int sampleSize) throws IOException {
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
throw new IOException("APNG dimensions exceed safe limits: " + width + "x" + height);
}
int capacity;
try {
int ss = Math.multiplyExact(sampleSize, sampleSize);
int canvasSize = Math.multiplyExact(width, height);
int pixelCount = canvasSize / ss + 1;
if (pixelCount <= 0 || pixelCount > MAX_TOTAL_PIXELS) {
throw new IOException("APNG pixel count exceeds safe limits: " + pixelCount);
}
capacity = Math.multiplyExact(pixelCount, 4);
} catch (ArithmeticException e) {
throw new IOException("Failed to multiply dimensions and sample size: " + width + "x" + height + " @ sample size " + sampleSize + " (overflow?)", e);
}
return capacity;
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.signal.glide.common.decode.Frame;
import org.signal.glide.load.resource.apng.io.APNGReader;
import org.signal.glide.load.resource.apng.io.APNGWriter;
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;
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import android.content.Context;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.StreamReader;
import org.signal.glide.load.resource.apng.io.APNGReader;
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) {
return false;
}
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;
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import android.text.TextUtils;
import org.signal.glide.load.resource.apng.io.APNGReader;
import java.io.IOException;
/**
* @Description: Length (长度) 4字节 指定数据块中数据域的长度,其长度不超过(2311)字节
* 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 {
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import org.signal.glide.load.resource.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();
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import org.signal.glide.load.resource.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();
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
/**
* @Description: 作用描述
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class IDATChunk extends Chunk {
static final int ID = fourCCToInt("IDAT");
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
/**
* @Description: 作用描述
* @Author: pengfei.zhou
* @CreateDate: 2019/3/27
*/
class IENDChunk extends Chunk {
static final int ID = Chunk.fourCCToInt("IEND");
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import org.signal.glide.load.resource.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);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.apng.decode;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import org.signal.glide.common.decode.Frame;
import org.signal.glide.load.resource.apng.io.APNGReader;
import org.signal.glide.load.resource.apng.io.APNGWriter;
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;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.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;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 Zhou Pengfei
* SPDX-License-Identifier: Apache-2.0
*/
package org.signal.glide.load.resource.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);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,351 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.signal.glide.load.resource.bitmap;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.bumptech.glide.util.Util;
import org.signal.core.util.logging.Log;
import java.io.File;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* State and constants for interacting with {@link android.graphics.Bitmap.Config#HARDWARE} on Android O+.
*/
public final class HardwareConfigState {
private static final String TAG = "HardwareConfig";
private static final boolean enableVerboseLogging = false;
/**
* Force the state to wait until a call to allow hardware Bitmaps to be used when they'd otherwise
* be eligible to work around a framework issue pre Q that can cause a native crash when
* allocating a hardware Bitmap in this specific circumstance. See b/126573603#comment12 for
* details.
*/
public static final boolean BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED =
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
/**
* Support for the hardware bitmap config was added in Android O.
*/
public static final boolean HARDWARE_BITMAPS_SUPPORTED =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
/**
* The minimum size in pixels a {@link Bitmap} must be in both dimensions to be created with the
* {@link Bitmap.Config#HARDWARE} configuration.
*
* <p>This is a quick check that lets us skip wasting FDs (see {@link #FD_SIZE_LIST}) on small
* {@link Bitmap}s with relatively low memory costs.
*
* @see #FD_SIZE_LIST
*/
@VisibleForTesting static final int MIN_HARDWARE_DIMENSION_O = 128;
private static final int MIN_HARDWARE_DIMENSION_P = 0;
/**
* Allows us to check to make sure we're not exceeding the FD limit for a process with hardware
* {@link Bitmap}s.
*
* <p>{@link Bitmap.Config#HARDWARE} {@link Bitmap}s require two FDs (depending on the driver).
* Processes have an FD limit of 1024 (at least on O). With sufficiently small {@link Bitmap}s
* and/or a sufficiently large {@link com.bumptech.glide.load.engine.cache.MemoryCache}, we can
* end up with enough {@link Bitmap}s in memory that we blow through the FD limit, which causes
* graphics errors, Binder errors, and a variety of crashes.
*
* <p>Calling list.size() should be relatively efficient (hopefully < 1ms on average) because
* /proc is an in-memory FS.
*/
private static final File FD_SIZE_LIST = new File("/proc/self/fd");
/**
* Each FD check takes 1-2ms, so to avoid overhead, only check every N decodes. 50 is more or less
* arbitrary.
*/
private static final int MINIMUM_DECODES_BETWEEN_FD_CHECKS = 50;
/**
* 700 with an error of 50 Bitmaps in between at two FDs each lets us use up to 800 FDs for
* hardware Bitmaps.
*
* <p>Prior to P, the limit per process was 1024 FDs. In P, the limit was updated to 32k FDs per
* process.
*
* <p>Access to this variable will be removed in a future version without deprecation.
*/
private static final int MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_O = 700;
// 20k.
private static final int MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P = 20000;
/**
* This constant will be removed in a future version without deprecation, avoid using it.
*/
public static final int NO_MAX_FD_COUNT = -1;
private static volatile HardwareConfigState instance;
@SuppressWarnings("FieldMayBeFinal")
private static volatile int manualOverrideMaxFdCount = NO_MAX_FD_COUNT;
private final boolean isHardwareConfigAllowedByDeviceModel;
private final int sdkBasedMaxFdCount;
private final int minHardwareDimension;
@GuardedBy("this")
private int decodesSinceLastFdCheck;
@GuardedBy("this")
private boolean isFdSizeBelowHardwareLimit = true;
/**
* Only mutated on the main thread. Read by any number of background threads concurrently.
*
* <p>Defaults to {@code false} because we need to wait for the GL context to be initialized and
* it defaults to not initialized (<a href="https://b.corp.google.com/issues/126573603#comment12">https://b.corp.google.com/issues/126573603#comment12</a>).
*/
private final AtomicBoolean isHardwareConfigAllowedByAppState = new AtomicBoolean(false);
public static HardwareConfigState getInstance() {
if (instance == null) {
synchronized (HardwareConfigState.class) {
if (instance == null) {
instance = new HardwareConfigState();
}
}
}
return instance;
}
@VisibleForTesting HardwareConfigState() {
isHardwareConfigAllowedByDeviceModel = isHardwareConfigAllowedByDeviceModel();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
sdkBasedMaxFdCount = MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_P;
minHardwareDimension = MIN_HARDWARE_DIMENSION_P;
} else {
sdkBasedMaxFdCount = MAXIMUM_FDS_FOR_HARDWARE_CONFIGS_O;
minHardwareDimension = MIN_HARDWARE_DIMENSION_O;
}
}
public boolean areHardwareBitmapsBlocked() {
Util.assertMainThread();
return !isHardwareConfigAllowedByAppState.get();
}
public void blockHardwareBitmaps() {
Util.assertMainThread();
isHardwareConfigAllowedByAppState.set(false);
}
public void unblockHardwareBitmaps() {
Util.assertMainThread();
isHardwareConfigAllowedByAppState.set(true);
}
public boolean isHardwareConfigAllowed(
int targetWidth,
int targetHeight,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired)
{
if (!isHardwareConfigAllowed) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by caller");
}
return false;
}
if (!isHardwareConfigAllowedByDeviceModel) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by device model");
}
return false;
}
if (!HARDWARE_BITMAPS_SUPPORTED) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by sdk");
}
return false;
}
if (areHardwareBitmapsBlockedByAppState()) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed by app state");
}
return false;
}
if (isExifOrientationRequired) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because exif orientation is required");
}
return false;
}
if (targetWidth < minHardwareDimension) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because width is too small");
}
return false;
}
if (targetHeight < minHardwareDimension) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because height is too small");
}
return false;
}
// Make sure to call isFdSizeBelowHardwareLimit last because it has side affects.
if (!isFdSizeBelowHardwareLimit()) {
if (enableVerboseLogging) {
Log.v(TAG, "Hardware config disallowed because there are insufficient FDs");
}
return false;
}
return true;
}
private boolean areHardwareBitmapsBlockedByAppState() {
return BLOCK_HARDWARE_BITMAPS_WHEN_GL_CONTEXT_MIGHT_NOT_BE_INITIALIZED
&& !isHardwareConfigAllowedByAppState.get();
}
@TargetApi(Build.VERSION_CODES.O)
boolean setHardwareConfigIfAllowed(
int targetWidth,
int targetHeight,
BitmapFactory.Options optionsWithScaling,
boolean isHardwareConfigAllowed,
boolean isExifOrientationRequired)
{
boolean result =
isHardwareConfigAllowed(
targetWidth, targetHeight, isHardwareConfigAllowed, isExifOrientationRequired);
if (result) {
optionsWithScaling.inPreferredConfig = Bitmap.Config.HARDWARE;
optionsWithScaling.inMutable = false;
}
return result;
}
private static boolean isHardwareConfigAllowedByDeviceModel() {
return !isHardwareConfigDisallowedByB112551574() && !isHardwareConfigDisallowedByB147430447();
}
private static boolean isHardwareConfigDisallowedByB147430447() {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
return false;
}
// This method will only be called once, so simple iteration is reasonable.
return Arrays.asList(
"LG-M250",
"LG-M320",
"LG-Q710AL",
"LG-Q710PL",
"LGM-K121K",
"LGM-K121L",
"LGM-K121S",
"LGM-X320K",
"LGM-X320L",
"LGM-X320S",
"LGM-X401L",
"LGM-X401S",
"LM-Q610.FG",
"LM-Q610.FGN",
"LM-Q617.FG",
"LM-Q617.FGN",
"LM-Q710.FG",
"LM-Q710.FGN",
"LM-X220PM",
"LM-X220QMA",
"LM-X410PM")
.contains(Build.MODEL);
}
private static boolean isHardwareConfigDisallowedByB112551574() {
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O) {
return false;
}
// This method will only be called once, so simple iteration is reasonable.
for (String prefixOrModelName :
// This is sadly a list of prefixes, not models. We no longer have the data that shows us
// all the explicit models, so we have to live with the prefixes.
Arrays.asList(
// Samsung
"SC-04J",
"SM-N935",
"SM-J720",
"SM-G570F",
"SM-G570M",
"SM-G960",
"SM-G965",
"SM-G935",
"SM-G930",
"SM-A520",
"SM-A720F",
// Moto
"moto e5",
"moto e5 play",
"moto e5 plus",
"moto e5 cruise",
"moto g(6) forge",
"moto g(6) play")) {
if (Build.MODEL.startsWith(prefixOrModelName)) {
return true;
}
}
return false;
}
private int getMaxFdCount() {
return manualOverrideMaxFdCount != NO_MAX_FD_COUNT
? manualOverrideMaxFdCount
: sdkBasedMaxFdCount;
}
@SuppressLint("LogTagInlined")
private synchronized boolean isFdSizeBelowHardwareLimit() {
if (++decodesSinceLastFdCheck >= MINIMUM_DECODES_BETWEEN_FD_CHECKS) {
decodesSinceLastFdCheck = 0;
int currentFds = Objects.requireNonNull(FD_SIZE_LIST.list()).length;
long maxFdCount = getMaxFdCount();
isFdSizeBelowHardwareLimit = currentFds < maxFdCount;
if (!isFdSizeBelowHardwareLimit && enableVerboseLogging) {
Log.w(
Downsampler.TAG,
"Excluding HARDWARE bitmap config because we're over the file descriptor limit"
+ ", file descriptors "
+ currentFds
+ ", limit "
+ maxFdCount);
}
}
return isFdSizeBelowHardwareLimit;
}
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.load.resource.bitmap;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.ByteBufferUtil;
import com.bumptech.glide.util.Preconditions;
import org.signal.glide.common.io.InputStreamFactory;
import org.signal.glide.common.io.InputStreamRewinder;
import org.signal.glide.load.ImageHeaderParserUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.List;
/**
* This is a helper class for {@link Downsampler} that abstracts out image operations from the input
* type wrapped into a {@link DataRewinder}.
*/
interface ImageReader {
@Nullable
Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException;
ImageHeaderParser.ImageType getImageType() throws IOException;
int getImageOrientation() throws IOException;
void stopGrowingBuffers();
final class ByteArrayReader implements ImageReader {
private final byte[] bytes;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
ByteArrayReader(byte[] bytes, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.bytes = bytes;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
return BitmapFactory.decodeByteArray(bytes, /* offset= */ 0, bytes.length, options);
}
@Override
public ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, ByteBuffer.wrap(bytes));
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBuffer.wrap(bytes), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
}
final class FileReader implements ImageReader {
private final File file;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
FileReader(File file, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.file = file;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) throws FileNotFoundException {
InputStream is = null;
try {
is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool);
return BitmapFactory.decodeStream(is, /* outPadding= */ null, options);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
}
@Override
public ImageType getImageType() throws IOException {
InputStream is = null;
try {
is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool);
return ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
// Ignored.
}
}
}
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, InputStreamFactory.build(file), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
}
final class ByteBufferReader implements ImageReader {
private final ByteBuffer buffer;
private final List<ImageHeaderParser> parsers;
private final ArrayPool byteArrayPool;
ByteBufferReader(ByteBuffer buffer, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.buffer = buffer;
this.parsers = parsers;
this.byteArrayPool = byteArrayPool;
}
@Nullable
@Override
public Bitmap decodeBitmap(Options options) {
return BitmapFactory.decodeStream(stream(), /* outPadding= */ null, options);
}
@Override
public ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, ByteBufferUtil.rewind(buffer));
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBufferUtil.rewind(buffer), byteArrayPool);
}
@Override
public void stopGrowingBuffers() {}
private InputStream stream() {
return ByteBufferUtil.toStream(ByteBufferUtil.rewind(buffer));
}
}
final class InputStreamImageReader implements ImageReader {
private final InputStreamRewinder dataRewinder;
private final ArrayPool byteArrayPool;
private final List<ImageHeaderParser> parsers;
private final InputStreamFactory inputStreamFactory;
InputStreamImageReader(@NonNull InputStreamFactory inputStreamFactory, List<ImageHeaderParser> parsers, ArrayPool byteArrayPool) {
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
this.parsers = Preconditions.checkNotNull(parsers);
this.inputStreamFactory = inputStreamFactory;
this.dataRewinder = new InputStreamRewinder(inputStreamFactory.create(), byteArrayPool);
}
@Nullable
@Override
public Bitmap decodeBitmap(@NonNull BitmapFactory.Options options) {
try {
return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
} catch (IOException e) {
return BitmapFactory.decodeStream(inputStreamFactory.createRecyclable(byteArrayPool), null, options);
}
}
@Override
public ImageHeaderParser.ImageType getImageType() throws IOException {
try {
return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
} catch (IOException e) {
return ImageHeaderParserUtils.getType(parsers, inputStreamFactory.createRecyclable(byteArrayPool), byteArrayPool);
}
}
@Override
public int getImageOrientation() throws IOException {
try {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool, true);
} catch (IOException e) {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, inputStreamFactory, byteArrayPool);
}
}
@Override
public void stopGrowingBuffers() {
dataRewinder.fixMarkLimits();
}
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) final class ParcelFileDescriptorImageReader implements ImageReader {
private final ArrayPool byteArrayPool;
private final List<ImageHeaderParser> parsers;
private final ParcelFileDescriptorRewinder dataRewinder;
ParcelFileDescriptorImageReader(
ParcelFileDescriptor parcelFileDescriptor,
List<ImageHeaderParser> parsers,
ArrayPool byteArrayPool)
{
this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool);
this.parsers = Preconditions.checkNotNull(parsers);
dataRewinder = new ParcelFileDescriptorRewinder(parcelFileDescriptor);
}
@Nullable
@Override
public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException {
return BitmapFactory.decodeFileDescriptor(
dataRewinder.rewindAndGet().getFileDescriptor(), null, options);
}
@Override
public ImageHeaderParser.ImageType getImageType() throws IOException {
return ImageHeaderParserUtils.getType(parsers, dataRewinder, byteArrayPool);
}
@Override
public int getImageOrientation() throws IOException {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder, byteArrayPool);
}
@Override
public void stopGrowingBuffers() {
// Nothing to do here.
}
}
}