diff --git a/app/build.gradle b/app/build.gradle index 3ebaf512b..789d83201 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { defaultConfig { applicationId "moe.tarsin.ehviewer" - minSdkVersion 31 + minSdkVersion 28 targetSdkVersion 33 versionCode 180020 versionName "1.8.3.0-beta02" diff --git a/app/src/main/cpp/archive.c b/app/src/main/cpp/archive.c index 2b8dbdb74..bc61cc7a6 100644 --- a/app/src/main/cpp/archive.c +++ b/app/src/main/cpp/archive.c @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -249,8 +250,8 @@ Java_com_hippo_UriArchiveAccessor_openArchive(JNIEnv *env, jobject thiz, jint fd return r; } -JNIEXPORT jlong JNICALL -Java_com_hippo_UriArchiveAccessor_extractToAddr(JNIEnv *env, jobject thiz, jint index) { +JNIEXPORT jint JNICALL +Java_com_hippo_UriArchiveAccessor_extractToMemfd(JNIEnv *env, jobject thiz, jint index) { EH_UNUSED(env); EH_UNUSED(thiz); archive_ctx *ctx = NULL; @@ -258,24 +259,13 @@ Java_com_hippo_UriArchiveAccessor_extractToAddr(JNIEnv *env, jobject thiz, jint ret = archive_get_ctx(&ctx, index); if (ret) return 0; - long long size = archive_entry_size(ctx->entry); - void *buffer = malloc(size + sizeof(long long)); - if (!buffer) { - ctx->using = 0; - LOGE("Allocate buffer for decompression failed:ENOMEM"); - return 0; - } - *(long long *) buffer = size; - ret = archive_read_data(ctx->arc, buffer + sizeof(long long), size); + int memfd = syscall(SYS_memfd_create, "ArchiveMemFD", 0); + ret = archive_read_data_into_fd(ctx->arc, memfd); ctx->using = 0; - if (ret == size) - return (jlong) buffer; - if (ret != size) - LOGE("%s", "No enough data read, WTF?"); if (ret < 0) LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc)); - free(buffer); - return 0; + lseek64(memfd, 0, SEEK_SET); + return memfd; } JNIEXPORT void JNICALL diff --git a/app/src/main/cpp/image.c b/app/src/main/cpp/image.c index e0a21d16c..a210c5560 100644 --- a/app/src/main/cpp/image.c +++ b/app/src/main/cpp/image.c @@ -20,58 +20,21 @@ #include #include +#include #include -#include #include #include +#include #define TAG "ImageDecoder_wrapper" -#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, TAG ,__VA_ARGS__) -#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG ,__VA_ARGS__) -#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG ,__VA_ARGS__) -#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG ,__VA_ARGS__) -#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG ,__VA_ARGS__) -#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, TAG ,__VA_ARGS__) + +#include "ehviewer.h" #define IMAGE_TILE_MAX_SIZE (512 * 512) static char tile_buffer[IMAGE_TILE_MAX_SIZE * 4]; -typedef struct { - AImageDecoder *decoder; - void *srcBuffer; - size_t stride; - int fd; -} SOURCE; - -typedef struct { - int32_t width; - int32_t height; - void *buffer; - int bufferLen; - bool isAnimated; - bool alpha; - int delay; - SOURCE *src; -} IMAGE; - -static void recycleSource(SOURCE *src) { - if (!src) - return; - AImageDecoder_delete(src->decoder); - free(src->srcBuffer); - free(src); -} - -static void recycle(IMAGE *image) { - if (!image) - return; - recycleSource(image->src); - free(image->buffer); - free(image); -} - bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y, void *dst, int dst_w, int dst_h, int dst_x, int dst_y, int width, int height) { @@ -159,138 +122,40 @@ bool copy_pixels(const void *src, int src_w, int src_h, int src_x, int src_y, return true; } -void decodeCommon(IMAGE *image, SOURCE *src) { - image->src = src; - AImageDecoder *decoder = src->decoder; - image->isAnimated = AImageDecoder_isAnimated(decoder); - AImageDecoder_setDataSpace(decoder, ADATASPACE_SRGB); - AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGBA_8888); - const AImageDecoderHeaderInfo *headerInfo = AImageDecoder_getHeaderInfo(decoder); - image->height = AImageDecoderHeaderInfo_getHeight(headerInfo); - image->width = AImageDecoderHeaderInfo_getWidth(headerInfo); - image->alpha = - AImageDecoderHeaderInfo_getAlphaFlags(headerInfo) == ANDROID_BITMAP_FLAGS_ALPHA_PREMUL; - src->stride = AImageDecoder_getMinimumStride(decoder); - image->bufferLen = (int) (image->height * src->stride); - image->buffer = malloc(image->bufferLen); - if (image->isAnimated) { - AImageDecoder_setInternallyHandleDisposePrevious(decoder, false); - AImageDecoderFrameInfo *frameInfo = AImageDecoderFrameInfo_create(); - AImageDecoder_getFrameInfo(decoder, frameInfo); - image->delay = (int) AImageDecoderFrameInfo_getDuration(frameInfo) / 1000000; - AImageDecoderFrameInfo_delete(frameInfo); - } - AImageDecoder_decodeImage(decoder, image->buffer, src->stride, image->bufferLen); - if (!image->isAnimated) { - recycleSource(image->src); - image->src = NULL; - } -} - -IMAGE *createFromFd(int fd) { - AImageDecoder *decoder = NULL; - if (AImageDecoder_createFromFd(fd, &decoder) != ANDROID_IMAGE_DECODER_SUCCESS) - return NULL; - IMAGE *image = calloc(1, sizeof(IMAGE)); - SOURCE *src = calloc(1, sizeof(SOURCE)); - src->decoder = decoder; - src->fd = fd; - decodeCommon(image, src); - return image; -} - -IMAGE *createFromAddr(void *addr, long long size) { - AImageDecoder *decoder; - AImageDecoder_createFromBuffer((const void *) addr + sizeof(long long), size, &decoder); - IMAGE *image = calloc(1, sizeof(IMAGE)); - SOURCE *src = calloc(1, sizeof(SOURCE)); - src->decoder = decoder; - src->srcBuffer = addr; - decodeCommon(image, src); - return image; -} - -IMAGE *create(int32_t width, int32_t height, const void *data) { - IMAGE *plain = calloc(1, sizeof(IMAGE)); - void *buffer = NULL; - size_t length = width * height * 4; - - buffer = malloc(length); - memcpy(buffer, data, length); - - // plain->isAnimated = false; this is unnecessary since we use calloc - plain->width = width; - plain->height = height; - plain->buffer = buffer; - plain->bufferLen = (int) length; - - return plain; -} - -void render(IMAGE *image, int src_x, int src_y, - void *dst, int dst_w, int dst_h, int dst_x, int dst_y, - int width, int height) { - copy_pixels(image->buffer, image->width, image->height, src_x, src_y, dst, dst_w, dst_h, dst_x, - dst_y, width, height); -} - -void advance(IMAGE *image) { - if (image->isAnimated) { - AImageDecoder_advanceFrame(image->src->decoder); - if (AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride, - image->bufferLen) == ANDROID_IMAGE_DECODER_FINISHED) { - AImageDecoder_rewind(image->src->decoder); - AImageDecoder_decodeImage(image->src->decoder, image->buffer, image->src->stride, - image->bufferLen); - } - } -} - -JNIEXPORT jlong JNICALL -Java_com_hippo_image_Image_nativeDecode(JNIEnv *env, jclass clazz, jint fd) { - IMAGE *image = createFromFd(fd); - return (jlong) image; -} - -JNIEXPORT jlong JNICALL -Java_com_hippo_image_Image_nativeCreate(JNIEnv *env, jclass clazz, jobject bitmap) { - AndroidBitmapInfo info; - void *pixels = NULL; - void *image = NULL; - - AndroidBitmap_getInfo(env, bitmap, &info); - AndroidBitmap_lockPixels(env, bitmap, &pixels); - - image = create((int) info.width, (int) info.height, pixels); - AndroidBitmap_unlockPixels(env, bitmap); - return (jlong) image; -} - JNIEXPORT void JNICALL -Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jlong ptr, jint src_x, +Java_com_hippo_image_Image_nativeRender(JNIEnv *env, jclass clazz, jobject srcBitmap, jint src_x, jint src_y, jobject dst, jint dst_x, jint dst_y, jint width, jint height) { - AndroidBitmapInfo info; - void *pixels = NULL; + AndroidBitmapInfo dstInfo; + AndroidBitmapInfo srcInfo; + void *srcPixels = NULL; + void *dstPixels = NULL; - AndroidBitmap_getInfo(env, dst, &info); - AndroidBitmap_lockPixels(env, dst, &pixels); + AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels); + AndroidBitmap_lockPixels(env, dst, &dstPixels); + AndroidBitmap_getInfo(env, srcBitmap, &srcInfo); + AndroidBitmap_getInfo(env, dst, &dstInfo); - render((IMAGE *) ptr, src_x, src_y, pixels, (int) info.width, (int) info.height, dst_x, dst_y, - width, - height); + copy_pixels(srcPixels, srcInfo.width, srcInfo.height, src_x, src_y, dstPixels, dstInfo.width, + dstInfo.height, dst_x, dst_y, + width, + height); AndroidBitmap_unlockPixels(env, dst); + AndroidBitmap_unlockPixels(env, srcBitmap); } JNIEXPORT void JNICALL -Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr, jboolean init, - jint src_x, jint src_y, jint width, jint height) { +Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jobject bitmap, jboolean init, + jint offset_x, jint offset_y, jint width, jint height) { if (width * height > IMAGE_TILE_MAX_SIZE) return; - - render((IMAGE *) ptr, src_x, src_y, tile_buffer, width, height, 0, 0, width, height); - + AndroidBitmapInfo info; + void *pixels = NULL; + AndroidBitmap_lockPixels(env, bitmap, &pixels); + AndroidBitmap_getInfo(env, bitmap, &info); + copy_pixels(pixels, info.width, info.height, offset_x, offset_y, tile_buffer, width, height, 0, 0, width, height); + AndroidBitmap_unlockPixels(env, bitmap); if (init) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, tile_buffer); @@ -299,44 +164,3 @@ Java_com_hippo_image_Image_nativeTexImage(JNIEnv *env, jclass clazz, jlong ptr, tile_buffer); } } - -JNIEXPORT void JNICALL -Java_com_hippo_image_Image_nativeAdvance(JNIEnv *env, jclass clazz, jlong ptr) { - advance((IMAGE *) ptr); -} - -JNIEXPORT jint JNICALL -Java_com_hippo_image_Image_nativeGetDelay(JNIEnv *env, jclass clazz, jlong ptr) { - return ((IMAGE *) ptr)->delay; -} - -JNIEXPORT jboolean JNICALL -Java_com_hippo_image_Image_nativeIsOpaque(JNIEnv *env, jclass clazz, jlong ptr) { - return ((IMAGE *) ptr)->alpha; -} - -JNIEXPORT void JNICALL -Java_com_hippo_image_Image_nativeRecycle(JNIEnv *env, jclass clazz, jlong ptr) { - recycle((IMAGE *) ptr); -} - -JNIEXPORT jlong JNICALL -Java_com_hippo_image_Image_nativeDecodeAddr(JNIEnv *env, jclass clazz, jlong addr) { - IMAGE *image = createFromAddr((void *) addr, *(long long *) addr); - return (jlong) image; -} - -JNIEXPORT jint JNICALL -Java_com_hippo_image_Image_nativeGetFormat(JNIEnv *env, jclass clazz, jlong native_ptr) { - return ((IMAGE *) native_ptr)->isAnimated; -} - -JNIEXPORT jint JNICALL -Java_com_hippo_image_Image_nativeGetWidth(JNIEnv *env, jclass clazz, jlong native_ptr) { - return ((IMAGE *) native_ptr)->width; -} - -JNIEXPORT jint JNICALL -Java_com_hippo_image_Image_nativeGetHeight(JNIEnv *env, jclass clazz, jlong native_ptr) { - return ((IMAGE *) native_ptr)->height; -} diff --git a/app/src/main/java/com/hippo/UriArchiveAccessor.kt b/app/src/main/java/com/hippo/UriArchiveAccessor.kt index 84aef7a9c..a1bfd1d05 100644 --- a/app/src/main/java/com/hippo/UriArchiveAccessor.kt +++ b/app/src/main/java/com/hippo/UriArchiveAccessor.kt @@ -27,7 +27,7 @@ class UriArchiveAccessor(ctx: Context, uri: Uri) { } private external fun openArchive(fd: Int, size: Long): Int - external fun extractToAddr(index: Int): Long + external fun extractToMemfd(index: Int): Int external fun extractToFd(index: Int, fd: Int) external fun getFilename(index: Int): String external fun needPassword(): Boolean diff --git a/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java b/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java index d6e53041b..efdddb61a 100644 --- a/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java +++ b/app/src/main/java/com/hippo/drawable/UnikeryDrawable.java @@ -16,6 +16,7 @@ package com.hippo.drawable; +import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Drawable; import android.util.Log; @@ -25,7 +26,6 @@ import com.hippo.conaco.Conaco; import com.hippo.conaco.ConacoTask; import com.hippo.conaco.Unikery; import com.hippo.image.ImageBitmap; -import com.hippo.image.ImageDrawable; import com.hippo.widget.ObservedTextView; public class UnikeryDrawable extends WrapDrawable implements Unikery, @@ -37,6 +37,8 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery conaco) { mTextView = textView; mTextView.setOnWindowAttachListener(this); @@ -62,11 +64,11 @@ public class UnikeryDrawable extends WrapDrawable implements Unikery { try { isPipe.obtain(); FileInputStream is = (FileInputStream) isPipe.open(); - return ImageBitmap.decode(is.getFD()); + return ImageBitmap.decode(is); } catch (OutOfMemoryError | IOException e) { return null; } finally { diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java index 659b793a5..9690a617e 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/ArchiveGalleryProvider.java @@ -19,6 +19,7 @@ package com.hippo.ehviewer.gallery; import android.content.Context; import android.net.Uri; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.Process; import androidx.annotation.NonNull; @@ -35,6 +36,8 @@ import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.thread.PVLock; import com.hippo.yorozuya.thread.PriorityThread; +import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Iterator; @@ -51,7 +54,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { public static PVLock pv = new PVLock(0); private final UriArchiveAccessor archiveAccessor; private final Stack requests = new Stack<>(); - private final LinkedHashMap streams = new LinkedHashMap<>(); + private final LinkedHashMap streams = new LinkedHashMap<>(); private Thread archiveThread; private Thread[] decodeThread = new Thread[] { new Thread(new DecodeTask()), @@ -298,10 +301,10 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { } } - long addr = archiveAccessor.extractToAddr(index); + int fd = archiveAccessor.extractToMemfd(index); synchronized (streams) { - streams.put(index, addr); + streams.put(index, fd); streams.notify(); } } @@ -313,7 +316,7 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { public void run() { while (!Thread.currentThread().isInterrupted()) { int index; - Long addr; + int fd; synchronized (streams) { if (streams.isEmpty()) { try { @@ -324,14 +327,15 @@ public class ArchiveGalleryProvider extends GalleryProvider2 { continue; } - Iterator> iterator = streams.entrySet().iterator(); - Map.Entry entry = iterator.next(); + Iterator> iterator = streams.entrySet().iterator(); + Map.Entry entry = iterator.next(); iterator.remove(); index = entry.getKey(); - addr = entry.getValue(); + fd = entry.getValue(); } - Image image = Image.decodeAddr(addr, true); + Image image; + image = Image.decode(new FileInputStream(ParcelFileDescriptor.adoptFd(fd).getFileDescriptor()), false); // we take ownership here since this memfd is anon fd if (image != null) { notifyPageSucceed(index, image); } else { diff --git a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java b/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java index ae5ae2efb..189373f4d 100644 --- a/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java +++ b/app/src/main/java/com/hippo/ehviewer/gallery/DirGalleryProvider.java @@ -259,7 +259,7 @@ public class DirGalleryProvider extends GalleryProvider2 implements Runnable { InputStream is = null; try { is = files[index].openInputStream(); - Image image = Image.decode(((FileInputStream) is).getFD(), true); + Image image = Image.decode((FileInputStream) is, false); mDecodingIndex.lazySet(GalleryPageView.INVALID_INDEX); if (image != null) { notifyPageSucceed(index, image); diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java index 8bd6de164..6eb4f76ba 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java @@ -1678,9 +1678,7 @@ public final class SpiderQueen implements Runnable { if (is != null) { try { - image = Image.decode(((FileInputStream) is).getFD(), true); - } catch (IOException e) { - e.printStackTrace(); + image = Image.decode(((FileInputStream) is), false); } finally { try { is.close(); diff --git a/app/src/main/java/com/hippo/glgallery/GalleryProvider.java b/app/src/main/java/com/hippo/glgallery/GalleryProvider.java index 0bf068875..12ef39820 100644 --- a/app/src/main/java/com/hippo/glgallery/GalleryProvider.java +++ b/app/src/main/java/com/hippo/glgallery/GalleryProvider.java @@ -57,7 +57,6 @@ public abstract class GalleryProvider { public void stop() { OSUtils.checkMainLoop(); mImageCache.evictAll(); - Image.lazyEvictAll(); } public void setGLRoot(GLRoot glRoot) { @@ -246,7 +245,7 @@ public abstract class GalleryProvider { } public void add(Integer key, ImageWrapper value) { - if (value.getFormat() != Image.FORMAT_ANIMATED) + if (!value.getAnimated()) if (value.obtain()) put(key, value); } diff --git a/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java b/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java index 898aa3b3c..b48e6a940 100644 --- a/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java +++ b/app/src/main/java/com/hippo/glview/image/ImageMovableTextTexture.java @@ -112,7 +112,6 @@ public class ImageMovableTextTexture extends ImageSpriteTexture { } Image image = Image.create(bitmap); - bitmap.recycle(); if (image == null) { return null; } diff --git a/app/src/main/java/com/hippo/glview/image/ImageTexture.java b/app/src/main/java/com/hippo/glview/image/ImageTexture.java index 3096ffd3e..ba444d252 100644 --- a/app/src/main/java/com/hippo/glview/image/ImageTexture.java +++ b/app/src/main/java/com/hippo/glview/image/ImageTexture.java @@ -233,7 +233,7 @@ public class ImageTexture implements Texture, Animatable { } boolean end = mReleased.get() || mImage.isImageRecycled() || mNeedRelease.get() || - (mImage.getFormat() == 0) || mRunning.get(); + (!mImage.getAnimated()) || mRunning.get(); synchronized (mImage) { mImageBusy = false; @@ -595,7 +595,7 @@ public class ImageTexture implements Texture, Animatable { // Release image mImageBusy = false; // Check need release, frameCount <= 1 - if (mNeedRelease.get() || mImage.getFormat() == 0) { + if (mNeedRelease.get() || !mImage.getAnimated()) { mAnimateRunnable = null; return; } @@ -617,7 +617,7 @@ public class ImageTexture implements Texture, Animatable { mImageBusy = true; } - mImage.advance(); + mImage.start(); long delay = mImage.getDelay(); long time = System.nanoTime(); if (-1L != lastDelay) { diff --git a/app/src/main/java/com/hippo/glview/image/ImageWrapper.java b/app/src/main/java/com/hippo/glview/image/ImageWrapper.java index 7a4e1f8e7..f4ef02ce9 100644 --- a/app/src/main/java/com/hippo/glview/image/ImageWrapper.java +++ b/app/src/main/java/com/hippo/glview/image/ImageWrapper.java @@ -102,14 +102,14 @@ public class ImageWrapper { } /** - * @see Image#getFormat() + * @see Image#getAnimated() */ - public int getFormat() { - return mImage.getFormat(); + public Boolean getAnimated() { + return mImage.getAnimated(); } /** - * @see Image#getFormat() + * @see Image#getAnimated() */ public int getWidth() { return mCut.width(); @@ -123,12 +123,12 @@ public class ImageWrapper { } /** - * @see Image#render(int, int, Bitmap, int, int, int, int, boolean, int) + * @see Image#render(int, int, Bitmap, int, int, int, int) */ public void render(int srcX, int srcY, Bitmap dst, int dstX, int dstY, int width, int height, boolean fillBlank, int defaultColor) { mImage.render(srcX + mCut.left, srcY + mCut.top, dst, dstX, dstY, - width, height, fillBlank, defaultColor); + width, height); } /** @@ -139,10 +139,10 @@ public class ImageWrapper { } /** - * @see Image#advance() + * @see Image#start() */ - public void advance() { - mImage.advance(); + public void start() { + mImage.start(); } /** diff --git a/app/src/main/java/com/hippo/image/Image.kt b/app/src/main/java/com/hippo/image/Image.kt index 2385b5fe3..dee87c29f 100644 --- a/app/src/main/java/com/hippo/image/Image.kt +++ b/app/src/main/java/com/hippo/image/Image.kt @@ -1,267 +1,186 @@ /* - * Copyright 2016 Hippo Seven + * Copyright 2022 Tarsin Norbin * - * 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 + * This file is part of EhViewer * - * http://www.apache.org/licenses/LICENSE-2.0 + * EhViewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. * - * 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. + * EhViewer is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with EhViewer. + * If not, see . */ package com.hippo.image +import android.content.res.Resources import android.graphics.Bitmap -import com.hippo.Native.getFd -import java.io.FileDescriptor -import java.util.function.Consumer - -/** - * The `Image` is a image which stored pixel data in native heap - */ -class Image private constructor(nativePtr: Long) { - private var mNativePtr: Long - private var needRecycle = false - private var nativeProcessCnt = 0 +import android.graphics.Canvas +import android.graphics.ImageDecoder +import android.graphics.ImageDecoder.ALLOCATOR_DEFAULT +import android.graphics.ImageDecoder.ALLOCATOR_SOFTWARE +import android.graphics.ImageDecoder.Source +import android.graphics.PixelFormat +import android.graphics.drawable.AnimatedImageDrawable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import androidx.core.graphics.component1 +import androidx.core.graphics.component2 +import androidx.core.graphics.component3 +import androidx.core.graphics.component4 +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable +import java.io.FileInputStream +import java.nio.channels.FileChannel +class Image private constructor(source: Source?, drawable: Drawable? = null, + val hardware: Boolean = true +) { + internal var mObtainedDrawable: Drawable? + private var mBitmap: Bitmap? = null init { - if (needEvictAll) evictAll() - mNativePtr = nativePtr - imageList.add(this) - } - - /** - * Return the format of the image - */ - val format: Int - get() { - checkRecycledAndLock() - val ret = nativeGetFormat(mNativePtr) - releaseNativeLock() - return ret + mObtainedDrawable = null + source?.let { + mObtainedDrawable = ImageDecoder.decodeDrawable(source) { decoder: ImageDecoder, _, _ -> + decoder.allocator = if (hardware) ALLOCATOR_DEFAULT else ALLOCATOR_SOFTWARE + // Sadly we must use software memory since we need copy it to tile buffer, fuck glgallery + // Idk it will cause how much performance regression + } // Should we lazy decode it? } - - /** - * Return the width of the image - */ - val width: Int - get() { - checkRecycledAndLock() - val ret = nativeGetWidth(mNativePtr) - releaseNativeLock() - return ret + if (mObtainedDrawable == null) { + mObtainedDrawable = drawable!! } - - /** - * Return the height of the image - */ - val height: Int - get() { - checkRecycledAndLock() - val ret = nativeGetHeight(mNativePtr) - releaseNativeLock() - return ret - } - - @Synchronized - private fun checkRecycledAndLock() { - check(mNativePtr != 0L) { "The image is recycled." } - nativeProcessCnt++ } - @Synchronized - private fun releaseNativeLock() { - nativeProcessCnt-- - if (needRecycle) recycle() - } + val animated = mObtainedDrawable is AnimatedImageDrawable + val width = mObtainedDrawable!!.intrinsicWidth + val height = mObtainedDrawable!!.intrinsicHeight + val isRecycled = mObtainedDrawable == null - /** - * Render the image to `Bitmap` - */ - fun render( - srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int, - width: Int, height: Int, fillBlank: Boolean, defaultColor: Int - ) { - checkRecycledAndLock() - nativeRender(mNativePtr, srcX, srcY, dst, dstX, dstY, width, height) - releaseNativeLock() - } + var started = false - /** - * Call `glTexImage2D` for init is true and - * call `glTexSubImage2D` for init is false. - * width * height must <= 512 * 512 or do nothing - */ - fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) { - checkRecycledAndLock() - nativeTexImage(mNativePtr, init, offsetX, offsetY, width, height) - releaseNativeLock() - } - - /** - * Move to next frame. Do nothing for non-animation image - */ - fun advance() { - checkRecycledAndLock() - nativeAdvance(mNativePtr) - releaseNativeLock() - } - - /** - * Return current frame delay. 0 for non-animation image - */ - val delay: Int - get() { - checkRecycledAndLock() - val delay = nativeGetDelay(mNativePtr) - releaseNativeLock() - return if (delay <= 10) 100 else delay - } - - /** - * Return is the image opaque - */ - val isOpaque: Boolean - get() { - checkRecycledAndLock() - val ret = nativeIsOpaque(mNativePtr) - releaseNativeLock() - return ret - } - - /** - * Free the native object associated with this image. - * It must be called when the image will not be used. - * The image can't be used after this method is called. - */ @Synchronized fun recycle() { - if (mNativePtr != 0L) { - if (nativeProcessCnt != 0) { - needRecycle = true - return - } - nativeRecycle(mNativePtr) - mNativePtr = 0 - imageList.remove(this) + if (mObtainedDrawable is AnimatedImageDrawable) { + (mObtainedDrawable as AnimatedImageDrawable?)?.stop() + } + if (mObtainedDrawable is BitmapDrawable) { + (mObtainedDrawable as BitmapDrawable?)?.bitmap?.recycle() + } + mObtainedDrawable?.callback = null + mObtainedDrawable = null + mBitmap?.recycle() + mBitmap = null + } + + private fun prepareBitmap() { + if (mBitmap != null) return + mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + } + + private fun updateBitmap() { + prepareBitmap() + val (oldLeft, oldTop, oldRight, oldBottom) = mObtainedDrawable!!.bounds + mObtainedDrawable!!.setBounds(0, 0, width, height) + mObtainedDrawable!!.draw(Canvas(mBitmap!!)) + mObtainedDrawable!!.setBounds(oldLeft, oldTop, oldRight, oldBottom) + } + + fun render( + srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int, + width: Int, height: Int + ) { + check(!hardware) { "Hardware buffer cannot be used in glgallery" } + val bitmap: Bitmap = if (animated) { + updateBitmap() + mBitmap!! + } else { + mObtainedDrawable!!.toBitmap() + } + nativeRender( + bitmap, + srcX, + srcY, + dst, + dstX, + dstY, + width, + height + ) + } + + fun texImage(init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int) { + check(!hardware) { "Hardware buffer cannot be used in glgallery" } + val bitmap: Bitmap = if (animated) { + updateBitmap() + mBitmap!! + } else { + mObtainedDrawable!!.toBitmap() + } + nativeTexImage( + bitmap, + init, + offsetX, + offsetY, + width, + height + ) + } + + fun start() { + if (!started) { + (mObtainedDrawable as AnimatedImageDrawable?)?.start() } } - /** - * Returns true if this image has been recycled. - */ - val isRecycled: Boolean - get() = mNativePtr == 0L + val delay: Int + get() { + if (animated) + return 100 + return 0 + } + + val isOpaque: Boolean + get() { + return mObtainedDrawable?.opacity == PixelFormat.OPAQUE + } companion object { - const val FORMAT_NORMAL = 0 - const val FORMAT_ANIMATED = 1 - private val imageList = ArrayList() - private var needEvictAll = false - private fun newFromAddr(addr: Long): Image? { - return if (addr == 0L) null else Image(addr) + @JvmStatic + fun decode(stream: FileInputStream, hardware: Boolean = true): Image { + val src = ImageDecoder.createSource( + stream.channel.map( + FileChannel.MapMode.READ_ONLY, 0, + stream.available().toLong() + ) + ) + return Image(src, hardware = hardware) } @JvmStatic - fun lazyEvictAll() { - needEvictAll = true + fun create(bitmap: Bitmap): Image { + return Image(null, bitmap.toDrawable(Resources.getSystem()), false) } - private fun evictAll() { - ArrayList(imageList).forEach( - Consumer { obj: Image -> obj.recycle() }) - needEvictAll = false - } - - /** - * Decode image from `InputStream` - */ - @JvmStatic - fun decode(fd: FileDescriptor?, partially: Boolean): Image? { - return newFromAddr(nativeDecode(getFd(fd))) - } - - /** - * Decode image from `InputStream` - */ - @JvmStatic - fun decode(fd: Int, partially: Boolean): Image? { - return newFromAddr(nativeDecode(fd)) - } - - /** - * Decode image from `InputStream` - */ - @JvmStatic - fun decodeAddr(addr: Long, partially: Boolean): Image? { - return newFromAddr(nativeDecodeAddr(addr)) - } - - /** - * Create a plain image from Bitmap - */ - @JvmStatic - fun create(bitmap: Bitmap): Image? { - return newFromAddr(nativeCreate(bitmap)) - } - - /** - * Return all un-recycled `Image` instance count. - * It is useful for debug. - */ - @JvmStatic - val imageCount: Int - get() = imageList.size - - @JvmStatic - private external fun nativeDecode(fd: Int): Long - - @JvmStatic - private external fun nativeDecodeAddr(addr: Long): Long - - @JvmStatic - private external fun nativeCreate(bitmap: Bitmap): Long - @JvmStatic private external fun nativeRender( - nativePtr: Long, + bitmap: Bitmap, srcX: Int, srcY: Int, dst: Bitmap, dstX: Int, dstY: Int, width: Int, height: Int ) @JvmStatic private external fun nativeTexImage( - nativePtr: Long, + bitmap: Bitmap, init: Boolean, offsetX: Int, offsetY: Int, width: Int, height: Int ) - - @JvmStatic - private external fun nativeAdvance(nativePtr: Long) - - @JvmStatic - private external fun nativeGetDelay(nativePtr: Long): Int - - @JvmStatic - private external fun nativeIsOpaque(nativePtr: Long): Boolean - - @JvmStatic - private external fun nativeRecycle(nativePtr: Long) - - @JvmStatic - private external fun nativeGetFormat(nativePtr: Long): Int - - @JvmStatic - private external fun nativeGetWidth(nativePtr: Long): Int - - @JvmStatic - private external fun nativeGetHeight(nativePtr: Long): Int } } \ No newline at end of file diff --git a/app/src/main/java/com/hippo/image/ImageBitmap.kt b/app/src/main/java/com/hippo/image/ImageBitmap.kt index 723d6bfcf..3d60fe9ff 100644 --- a/app/src/main/java/com/hippo/image/ImageBitmap.kt +++ b/app/src/main/java/com/hippo/image/ImageBitmap.kt @@ -1,82 +1,32 @@ /* - * Copyright 2016 Hippo Seven + * Copyright 2022 Tarsin Norbin * - * 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 + * This file is part of EhViewer * - * http://www.apache.org/licenses/LICENSE-2.0 + * EhViewer is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. * - * 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. + * EhViewer is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with EhViewer. + * If not, see . */ package com.hippo.image -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RectF -import android.graphics.drawable.Animatable -import android.os.Handler -import android.os.Looper -import java.io.FileDescriptor -import java.lang.ref.WeakReference +import android.graphics.drawable.Drawable +import java.io.FileInputStream -/** - * A image with [Image] for data and [Bitmap] for render. - */ -class ImageBitmap : Animatable, Runnable { - private val mBitmap: Bitmap - - /** - * Return the format of the image - */ - val format: Int - - /** - * Return image is opaque - */ - val isOpaque: Boolean - private val mCallbackSet: MutableSet> = LinkedHashSet() - private var mImage: Image? = null +class ImageBitmap private constructor(image: Image) { + private var mImage: Image? = image private var mReferences = 0 - private var mAnimationReferences = 0 - private var mRunning = false - private constructor(image: Image) { - val width = image.width - val height = image.height - mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - image.render(0, 0, mBitmap, 0, 0, width, height, false, 0) - format = image.format - isOpaque = image.isOpaque - if (format == 1) { - // For animated image, save image object - mImage = image - } else { - // Free the image - image.recycle() - } - } - - private constructor(bitmap: Bitmap) { - format = Image.FORMAT_NORMAL - mBitmap = bitmap - isOpaque = !bitmap.hasAlpha() - } - - /** - * Obtain the image bitmap - * - * @return false for the image is recycled and obtain failed - */ @Synchronized fun obtain(): Boolean { - return if (mBitmap.isRecycled) { + return if (mImage?.isRecycled == true) { false } else { ++mReferences @@ -84,220 +34,31 @@ class ImageBitmap : Animatable, Runnable { } } - /** - * Release the image bitmap - */ @Synchronized fun release() { --mReferences - if (mReferences <= 0 && !mBitmap.isRecycled) { - mBitmap.recycle() - if (mImage != null) { - mImage!!.recycle() - } + if (mReferences <= 0 && !mImage!!.isRecycled) { + mImage!!.recycle() + mImage = null } } - /** - * Add a callback for invalidating - */ - fun addCallback(callback: Callback) { - val iterator = mCallbackSet.iterator() - var c: Callback? - while (iterator.hasNext()) { - c = iterator.next().get() - if (c == null) { - // Remove from the set if the reference has been cleared or - // it can't be used. - iterator.remove() - } else if (c === callback) { - return - } - } - mCallbackSet.add(WeakReference(callback)) + fun getDrawable(): Drawable { + check(obtain()) { "Recycled!" } + return mImage!!.mObtainedDrawable as Drawable } - /** - * Remove a callback - */ - fun removeCallback(callback: Callback) { - val iterator = mCallbackSet.iterator() - var c: Callback? - while (iterator.hasNext()) { - c = iterator.next().get() - if (c == null) { - // Remove from the set if the reference has been cleared or - // it can't be used. - iterator.remove() - } else if (c === callback) { - iterator.remove() - return - } - } - } - - /** - * Return image width - */ val width: Int - get() = mBitmap.width + get() = mImage!!.width - /** - * Return image height - */ val height: Int - get() = mBitmap.height - - /** - * Return image is animated - */ - val isAnimated: Boolean - get() = mImage != null - - /** - * Draw image to canvas - */ - fun draw(canvas: Canvas, left: Float, top: Float, paint: Paint?) { - if (!mBitmap.isRecycled) { - canvas.drawBitmap(mBitmap, left, top, paint) - } - } - - /** - * Draw image to canvas - */ - fun draw(canvas: Canvas, src: Rect?, dst: Rect, paint: Paint?) { - if (!mBitmap.isRecycled) { - canvas.drawBitmap(mBitmap, src, dst, paint) - } - } - - /** - * Draw image to canvas - */ - fun draw(canvas: Canvas, src: Rect?, dst: RectF, paint: Paint?) { - if (!mBitmap.isRecycled) { - canvas.drawBitmap(mBitmap, src, dst, paint) - } - } - - /** - * `start()` and `stop()` is a pair - */ - override fun start() { - mAnimationReferences++ - if (mBitmap.isRecycled || mImage == null || mRunning) { - return - } - mRunning = true - HANDLER.postDelayed(this, Math.max(0, mImage!!.delay).toLong()) - } - - /** - * `start()` and `stop()` is a pair - */ - override fun stop() { - mAnimationReferences-- - if (mAnimationReferences <= 0) { - mRunning = false - HANDLER.removeCallbacks(this) - } - } - - override fun isRunning(): Boolean { - return mRunning - } - - private fun notifyUpdate(): Boolean { - var hasCallback = false - val iterator = mCallbackSet.iterator() - var callback: Callback? - while (iterator.hasNext()) { - callback = iterator.next().get() - if (callback != null) { - // Render bitmap int the first time - if (!hasCallback) { - hasCallback = true - mImage!!.render(0, 0, mBitmap, 0, 0, mImage!!.width, mImage!!.height, false, 0) - } - callback.invalidateImage(this) - } else { - // Remove from the set if the reference has been cleared or - // it can't be used. - iterator.remove() - } - } - return hasCallback - } - - override fun run() { - // Check recycled - if (mBitmap.isRecycled || mImage == null) { - mRunning = false - return - } - mImage!!.advance() - if (notifyUpdate()) { - if (mRunning) { - HANDLER.postDelayed(this, Math.max(0, mImage!!.delay).toLong()) - } - } else { - mRunning = false - } - } - - interface Callback { - fun invalidateImage(who: ImageBitmap?) - } + get() = mImage!!.height companion object { - private val HANDLER = Handler(Looper.getMainLooper()) - - /** - * Decode `InputStream`, then create image. - */ @JvmStatic - fun decode(fd: FileDescriptor): ImageBitmap? { - val image = Image.decode(fd, false) - return if (image != null) { - create(image) - } else { - null - } - } - - /** - * Create `ImageBitmap` from `Image`. - * It is not recommended. Use [.decode] if you can. - * - * @param image the image should not be used before and - * it must not be recycled. And the image should - * not be used directly anymore. - */ - fun create(image: Image): ImageBitmap? { - return if (!image.isRecycled) { - try { - ImageBitmap(image) - } catch (e: OutOfMemoryError) { - image.recycle() - null - } - } else { - null - } - } - - /** - * Create `ImageBitmap` from `Bitmap`. - * - * @param bitmap the bitmap should not be recycled - */ - fun create(bitmap: Bitmap): ImageBitmap? { - return if (!bitmap.isRecycled) { - ImageBitmap(bitmap) - } else { - null - } + fun decode(stream: FileInputStream): ImageBitmap { + val image = Image.decode(stream) + return ImageBitmap(image) } } } \ No newline at end of file diff --git a/app/src/main/java/com/hippo/image/ImageDrawable.kt b/app/src/main/java/com/hippo/image/ImageDrawable.kt deleted file mode 100644 index c7c277a8c..000000000 --- a/app/src/main/java/com/hippo/image/ImageDrawable.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2016 Hippo Seven - * - * 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 com.hippo.image - -import android.graphics.Canvas -import android.graphics.ColorFilter -import android.graphics.Paint -import android.graphics.PixelFormat -import android.graphics.drawable.Animatable -import android.graphics.drawable.Drawable - -/** - * A drawable to draw [ImageBitmap] - */ -class ImageDrawable(imageBitmap: ImageBitmap) : Drawable(), Animatable, ImageBitmap.Callback { - private val mImageBitmap: ImageBitmap - private val mPaint: Paint - - init { - if (!imageBitmap.obtain()) { - throw Exception() - } - mImageBitmap = imageBitmap - mPaint = Paint(Paint.FILTER_BITMAP_FLAG) - - // Add callback - imageBitmap.addCallback(this) - } - - fun recycle() { - mImageBitmap.removeCallback(this) - mImageBitmap.release() - } - - override fun draw(canvas: Canvas) { - mImageBitmap.draw(canvas, null, bounds, mPaint) - } - - override fun setAlpha(alpha: Int) { - mPaint.alpha = alpha - } - - override fun setColorFilter(colorFilter: ColorFilter?) { - mPaint.colorFilter = colorFilter - } - - override fun getOpacity(): Int { - return if (mImageBitmap.isOpaque) PixelFormat.OPAQUE else PixelFormat.UNKNOWN - } - - override fun getIntrinsicWidth(): Int { - return mImageBitmap.width - } - - override fun getIntrinsicHeight(): Int { - return mImageBitmap.height - } - - override fun start() { - mImageBitmap.start() - } - - override fun stop() { - mImageBitmap.stop() - } - - override fun isRunning(): Boolean { - return mImageBitmap.isRunning - } - - override fun invalidateImage(who: ImageBitmap?) { - invalidateSelf() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/hippo/widget/LoadImageView.java b/app/src/main/java/com/hippo/widget/LoadImageView.java index f470d08bf..fab228c79 100644 --- a/app/src/main/java/com/hippo/widget/LoadImageView.java +++ b/app/src/main/java/com/hippo/widget/LoadImageView.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.drawable.Animatable; +import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; @@ -39,7 +40,6 @@ import com.hippo.drawable.PreciselyClipDrawable; import com.hippo.ehviewer.EhApplication; import com.hippo.ehviewer.R; import com.hippo.image.ImageBitmap; -import com.hippo.image.ImageDrawable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -63,6 +63,7 @@ public class LoadImageView extends FixedAspectImageView implements Unikery