/* * 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 com.bumptech.glide.load.Key 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.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 a background color * - 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 backgroundColor: Int) : BitmapTransformation() { override fun equals(other: Any?): Boolean { if (other is CompositeWithOpaqueBackground) { return other.backgroundColor == backgroundColor } return false } override fun hashCode() = Util.hashCode(ID.hashCode(), Util.hashCode(backgroundColor)) override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(ID_BYTES) messageDigest.update(ByteBuffer.allocate(Int.SIZE_BYTES).putInt(backgroundColor).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 // Convert the background to a bitmap. val backgroundBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) backgroundBitmap.eraseColor(backgroundColor) // 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(Key.CHARSET) /** 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 } } }