Initial migrate to ImageDecoder Java api, and downgrade minsdk to 28

This commit is contained in:
tarsin committed 2022-11-19 22:17:08 +08:00
1 parent fbdab389c4
commit 54d11dea95
18 files changed
+266 -862

No files matched your search

+142 -223
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Image>()
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
}
}
+28 -267
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<WeakReference<Callback>> = 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)
}
}
}
@@ -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()
}
}