Display long bitmap - android

I need to load a very long (approx 15K px) black-and-white bitmap which would have screen width.
I tried several approaches and the best seems using Glide:
private fun loadGlideScreenWideCompress(context: Context, imageView: AppCompatImageView) {
imageView.adjustViewBounds = true
val params = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
imageView.layoutParams = params
GlideApp.with(context)
.load(imageRes)
.encodeFormat(Bitmap.CompressFormat.WEBP)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView)
}
The problem is - image quality is lost. The image is blurred and text is not readable.
I tried to use BitmapRegionDecoder. I did not see the quality loss or memory issues. However, it decodes just a portion of the image. I do not really understand how to use it: to decode next part on scroll event? This would be hard to implement. Measure the drawable height and passing full height to the BitmapRegionDecoder intuitively feels wrong because this is the decoder is just for regions.
Another issue is that the image is large and I need it to work for all screen sizes. If I take the largest possible size and then scale it, I would need to perform expensive bitmap creating operations and block the main thread potentially.
The conventional approach does not work and give OOM exceptions:
val bitmap = BitmapFactory.Options().run {
inJustDecodeBounds = true
inPreferredConfig = Bitmap.Config.ALPHA_8
inDensity = displayMetrics.densityDpi
BitmapFactory.decodeResource(context.resources, imageRes, this)
}
imageView.scaleType = ImageView.ScaleType.FIT_CENTER
imageView.setImageBitmap(bitmap)
Code with scaling down:
val res = context.resources
val display = res.displayMetrics
val dr = res.getDrawable(imageRes!!)
val original = (dr as BitmapDrawable).bitmap
val scale = original.width / display.widthPixels
val scaledBitmap = BitmapDrawable(res, Bitmap.createScaledBitmap(
original,
display.widthPixels,
original.height / scale,
true
))
imageView.adjustViewBounds = true
val bos = ByteArrayOutputStream()
scaledBitmap.bitmap.compress(CompressFormat.WEBP, 100, bos)
val decoder = BitmapRegionDecoder.newInstance(
ByteArrayInputStream(bos.toByteArray()),
false
)
val rect = Rect(
0,
0,
scaledBitmap.intrinsicWidth,
scaledBitmap.intrinsicHeight
)
val bitmapFactoryOptions = BitmapFactory.Options()
bitmapFactoryOptions.inPreferredConfig = Bitmap.Config.ALPHA_8;
bitmapFactoryOptions.inDensity = display.densityDpi;
val bmp = decoder.decodeRegion(rect, bitmapFactoryOptions);
imageView.setImageBitmap(bmp)
So the question is: What would be the best approach in this situation and how to use BitmapRegionDecoder correctly?

Related

Is there a way to recycle instantly a bitmap?

I have a task to convert all pages of a PDF file to images and save it to cache. But when i create bitmap then recycle it, it seem not instantly be recycled. I think the problem is speed of create new bitmap is faster than recycle and then my app is crashed because OutOfMemoryError. Here is my example code to create image from a PDF:
for (index in 0 until pageCount) {
val pathImage = context?.cacheDir.toString() + index + TYPE_IMAGE
pdfiumCore.openPage(pdfDocument, index)
val width = pdfiumCore.getPageWidthPoint(pdfDocument, index)
var height = pdfiumCore.getPageHeightPoint(pdfDocument, index)
height = WIDTH_IMAGE_THUMB * height / width
val bitmap = Bitmap.createBitmap(WIDTH_IMAGE_THUMB, height, Bitmap.Config.ARGB_8888)
pdfiumCore.renderPageBitmap(pdfDocument, bitmap, index, 0, 0, WIDTH_IMAGE_THUMB, height)
images.add(ImageSelectPage(pathImage, false, index))
val fileOutputStream = FileOutputStream(pathImage)
bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY_CREATE_BITMAP, fileOutputStream)
bitmap.recycle() // here is recycle command
fileOutputStream.close()
index++
}
Don't know if there is a way to deal with this problem. I would like to hear any of your opinion. Thanks in advance!

How to properly grayscale bitmap on android to make the file lighter

I'm trying to get the most out of compression and I need to make my image grayscale in order to make the file lighter.
I found a function that makes image grayscale:
fun toGrayscale(bmpOriginal: Bitmap): Bitmap {
val width = bmpOriginal.width
val height = bmpOriginal.height
val bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val c = Canvas(bmpGrayscale)
val paint = Paint()
val cm = ColorMatrix()
cm.setSaturation(0f)
val f = ColorMatrixColorFilter(cm)
paint.colorFilter = f
c.drawBitmap(bmpOriginal, 0f, 0f, paint)
return bmpGrayscale
}
Then I save it with
try {
FileOutputStream(filePathGrayScale).use { out ->
bmp.compress(Bitmap.CompressFormat.JPEG, 100, out)
}
} catch (e: IOException) {
e.printStackTrace()
}
But the image size increase!
What exactly I'm doing wrong and how to make the thing correct?
bmp.compress(Bitmap.CompressFormat.JPEG, 100, out)
You are saving the image with an compression quality of 100. This maximizes file size (and minimizes quality loss).
If you want to make the most of compression, pass a lower value here. There is no ideal value because it really depends on the image being compressed and the tolerable amount of artefacts. It's a trade-of between file size and image quality.
For some more reading about the quality level see:
What quality to choose when converting to JPG?
And some examples 1, 2

What is the simplest way to get screenshot in android using kotlin?

I have an imageView and several textViews
My app allows user to drag textViews on evey coordinates of imageView (imageView is not full screen) that user wants .
In other words this app allows user to add several captions to user image
and convert that image and captions to a single image and store it on user device.
According to one of stackOverFlow responses I can just convert one textView text to a bitamp
id there any way to screenshot from final image which user have created with its captions in kotlin??
This is my code:
#Throws(IOException::class)
fun foo(text: String) {
val textPaint = object : Paint() {
init {
setColor(Color.WHITE)
setTextAlign(Align.CENTER)
setTextSize(20f)
setAntiAlias(true)
}
}
val bounds = Rect()
textPaint.getTextBounds(text, 0, text.length, bounds)
val bmp = Bitmap.createBitmap(mImgBanner.getWidth(), mImgBanner.getHeight(), Bitmap.Config.RGB_565) //use ARGB_8888 for better quality
val canvas = Canvas(bmp)
canvas.drawText(text, 0, 20f, textPaint)
val path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/image.png"
val stream = FileOutputStream(path)
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream)
bmp.recycle()
stream.close()
}
Add desired views in xml layout inflate it and take screenshot of parent layout that is containing your views.
Code for taking screenshoot:
fun takeScreenshotOfView(view: View, height: Int, width: Int): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val bgDrawable = view.background
if (bgDrawable != null) {
bgDrawable.draw(canvas)
} else {
canvas.drawColor(Color.WHITE)
}
view.draw(canvas)
return bitmap
}
you can also use the extension View.drawToBitmap(). It will return a Bitmap
/**
* Return a [Bitmap] representation of this [View].
*
* The resulting bitmap will be the same width and height as this view's current layout
* dimensions. This does not take into account any transformations such as scale or translation.
*
* Note, this will use the software rendering pipeline to draw the view to the bitmap. This may
* result with different drawing to what is rendered on a hardware accelerated canvas (such as
* the device screen).
*
* If this view has not been laid out this method will throw a [IllegalStateException].
*
* #param config Bitmap config of the desired bitmap. Defaults to [Bitmap.Config.ARGB_8888].
*/
fun View.drawToBitmap(config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
if (!ViewCompat.isLaidOut(this)) {
throw IllegalStateException("View needs to be laid out before calling drawToBitmap()")
}
return Bitmap.createBitmap(width, height, config).applyCanvas {
translate(-scrollX.toFloat(), -scrollY.toFloat())
draw(this)
}
}

Rotated bitmap is losing quality on every rotate

I am using the following function to rotate my bitmap. I noticed that if I rotate this bitmap a few times, it loses quality at every rotate upto a point that it becomes just a image with solid colour. I tried with PNG instead of JPEG but the size goes up drastically. Is there any solution to rotate the image without losing quality and increasing size?
fun rotatedImage(path:String):Bitmap{
val bitmap = BitmapFactory.decodeFile(path)
val mat = Matrix()
if (rotateDegree==270f) rotateDegree = 0f
mat.postRotate(rotateDegree+90)
val bMapRotate=Bitmap.createBitmap(bitmap, 0,0,bitmap.getWidth(),bitmap.getHeight(), mat, true)
deleteImageFile(path)
val fileStream = FileOutputStream(path)
// Tried with PNG as well = Size increases
bMapRotate.compress(Bitmap.CompressFormat.JPEG, 100, fileStream)
return bMapRotate
}

How to reuse existing bitmap memory with Bitmap.createBitmap()

I'm trying to optimise my app memory usage by following the recommended practice of using BitmapFactory.Options.inBitmap. What I want to do is slice a bitmap into stripes of height stripeHeight like this:
val stripeHeight = 10
val largeBitmap = // the original bitmap
val placeholder = // the bitmap object I want to re purpose for each stripe
val options = BitmapFactory.Options()
options.inBitmap = placeholder
val rows = largeBitmap.height / stripeHeight
for (i in 0 until rows) {
//how can I set the options object into createBitmap?
val stripe = Bitmap.createBitmap(largeBitmap, 0, i * stripeHeight, largeBitmap.width, stripeHeight)
it.onNext(stripe)
}
The problem is Bitmap.createBitmap won't take a BitmapFactory.Options in its method signature. So I'm unsure how to tell android to allocate the new bitmap into the memory space of the previous one.
One approach would be the following:
fun doBitmapProcessing() {
val STRIPE_HEIGHT = 10
//assuming your largeBitmap is a 320x240 ARGB8888 image
val largeBitmap: Bitmap = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888)
//then allocate a buffer of size 320 * 240 * 4 (4 channels in an ARGB image) bytes
val byteBuffer: ByteBuffer = ByteBuffer.allocate(320 * 240 * 4)
//create a bitmap of desired dimensions for the placeholder
val placeholder: Bitmap = Bitmap.createBitmap(320, STRIPE_HEIGHT, Bitmap.Config.ARGB_8888)
//copy the pixels from your large bitmap to a pre-allocated byte buffer
largeBitmap.copyPixelsToBuffer(byteBuffer)
//create an input stream from the pre-allocated buffer
val inputStream: InputStream = ByteArrayInputStream(byteBuffer.array())
//create the bitmap region decoder
val bitmapRegionDecoder: BitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, true)
//create options, as before
val options = BitmapFactory.Options()
options.inBitmap = placeholder
val rows = largeBitmap.height / STRIPE_HEIGHT
for (i in 0 until rows) {
//call decode stream on the input stream containing your data from *largeImage*, and the options
//in which you specify that you want the memory of *placeholder* to be overwritten.
val region: Rect = Rect(0, i * STRIPE_HEIGHT, largeBitmap.width, i * STRIPE_HEIGHT + STRIPE_HEIGHT)
val stripe: Bitmap = bitmapRegionDecoder.decodeRegion(region, options)
it.onNext(stripe)
}
}
Be sure to read the documentation of BitmapFactory.Options#inBitmap and use the result of BitmapRegionDecoder.decodeRegion(...) instead of placeholder directly, since it is not guaranteed that the same memory location will be used.
There might be another solution through Bitmap.getPixels(...) & Bitmap.setPixels:
fun doBitmapProcessing2() {
val STRIPE_HEIGHT = 10
//assuming your largeBitmap is in ARGB 8888 format
val largeBitmap: Bitmap = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888)
//create a bitmap of desired dimensions for the placeholder
val placeholder: Bitmap = Bitmap.createBitmap(320, STRIPE_HEIGHT, Bitmap.Config.ARGB_8888)
//create int pixel array in which the colors will be saved
val pixels = IntArray(320 * STRIPE_HEIGHT * 4)
val rows = largeBitmap.height / STRIPE_HEIGHT
for (i in 0 until rows) {
//save colors from desired location into the pixels array
largeBitmap.getPixels(pixels, 0, 320, 0, i * STRIPE_HEIGHT, 320, STRIPE_HEIGHT)
//write the colors saved into the array to the placeholder bitmap
placeholder.setPixels(pixels, 0, 320, 0, 0, 320, STRIPE_HEIGHT)
it.onNext(placeholder)
}
}
Although I'm not so sure about the second one. ( haven't used getPixels/setPixels before)
Hope this helps

Categories

Resources