/* * Copyright 2023 Tusky Contributors * * This file is a part of Tusky. * * This program 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. * * Tusky 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 Tusky; if not, * see . */ package com.keylesspalace.tusky.util import android.graphics.Bitmap import android.graphics.BitmapShader import android.graphics.Canvas import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.graphics.Paint import android.graphics.Shader import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.util.Log import android.util.TypedValue import android.view.View import androidx.annotation.AttrRes import androidx.core.content.ContextCompat import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.util.Util import java.nio.ByteBuffer import java.nio.charset.Charset import java.security.MessageDigest /** * Set an opaque background behind the non-transparent areas of a bitmap. * * Profile images may have areas that are partially transparent (i.e., alpha value >= 1 and < 255). * * Displaying those can be a problem if there is anything drawn under them, as it will show * through the image. * * Fix this, by: * * - Creating a mask that matches the partially transparent areas of the image * - Creating a new bitmap that, in the areas that match the mask, contains the same background * drawable as the [ImageView]. * - Composite the original image over the top * * So the partially transparent areas on the original image are composited over the original * background, the fully transparent areas on the original image are left transparent. */ class CompositeWithOpaqueBackground(val view: View) : BitmapTransformation() { override fun equals(other: Any?): Boolean { if (other is CompositeWithOpaqueBackground) { return other.view == view } return false } override fun hashCode() = Util.hashCode(ID.hashCode(), view.hashCode()) override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(ID_BYTES) messageDigest.update(ByteBuffer.allocate(4).putInt(view.hashCode()).array()) } override fun transform( pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int ): Bitmap { // If the input bitmap has no alpha channel then there's nothing to do if (!toTransform.hasAlpha()) return toTransform Log.d(TAG, "toTransform: ${toTransform.width} ${toTransform.height}") // Get the background drawable for this view, falling back to the given attribute val backgroundDrawable = view.getFirstNonNullBackgroundOrAttr(android.R.attr.colorBackground) backgroundDrawable ?: return toTransform // Convert the background to a bitmap. val backgroundBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) when (backgroundDrawable) { is ColorDrawable -> backgroundBitmap.eraseColor(backgroundDrawable.color) else -> { val backgroundCanvas = Canvas(backgroundBitmap) backgroundDrawable.setBounds(0, 0, outWidth, outHeight) backgroundDrawable.draw(backgroundCanvas) } } // Convert the alphaBitmap (where the alpha channel has 8bpp) to a mask of 1bpp // TODO: toTransform.extractAlpha(paint, ...) could be used here, but I can't find any // useful documentation covering paints and mask filters. val maskBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ALPHA_8).apply { val canvas = Canvas(this) canvas.drawBitmap(toTransform, 0f, 0f, EXTRACT_MASK_PAINT) } val shader = BitmapShader(backgroundBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) val paintShader = Paint() paintShader.isAntiAlias = true paintShader.shader = shader paintShader.style = Paint.Style.FILL_AND_STROKE // Write the background to a new bitmap, masked to just the non-transparent areas of the // original image val dest = pool.get(outWidth, outHeight, toTransform.config) val canvas = Canvas(dest) canvas.drawBitmap(maskBitmap, 0f, 0f, paintShader) // Finally, write the original bitmap over the top canvas.drawBitmap(toTransform, 0f, 0f, null) // Clean up intermediate bitmaps pool.put(maskBitmap) pool.put(backgroundBitmap) return dest } companion object { @Suppress("unused") private const val TAG = "CompositeWithOpaqueBackground" private val ID = CompositeWithOpaqueBackground::class.qualifiedName!! private val ID_BYTES = ID.toByteArray(Charset.forName("UTF-8")) /** Paint with a color filter that converts 8bpp alpha images to a 1bpp mask */ private val EXTRACT_MASK_PAINT = Paint().apply { colorFilter = ColorMatrixColorFilter( ColorMatrix( floatArrayOf( 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 255f, 0f ) ) ) isAntiAlias = false } /** * @param attr attribute reference for the default drawable if no background is set on * this view or any of its ancestors. * @return The first non-null background drawable from this view, or its ancestors, * falling back to the attribute resource given by `attr` if none of the views have a * background. */ fun View.getFirstNonNullBackgroundOrAttr(@AttrRes attr: Int): Drawable? = background ?: (parent as? View)?.getFirstNonNullBackgroundOrAttr(attr) ?: run { val v = TypedValue() context.theme.resolveAttribute(attr, v, true) // TODO: On API 29 can use v.isColorType here if (v.type >= TypedValue.TYPE_FIRST_COLOR_INT && v.type <= TypedValue.TYPE_LAST_COLOR_INT) { ColorDrawable(v.data) } else { ContextCompat.getDrawable(context, v.resourceId) } } } }