Transform & preserve gainmaps
Have Bitmap.createBitmap(sourceBitmap, ...) preserve any gainmaps if present, transforming the gainmaps in the same way. This addresses 2 common usages: 1) Rotating bitmaps to handle EXIF orientations 2) Bitmap.createScaledBitmap() to do "static" scaling Bug: 286131154 Test: SilkFX GainmapTransformsTest Change-Id: I5a62dccbb2c70bc38cca581b161eef792c8b2a78
This commit is contained in:
@@ -997,12 +997,63 @@ public final class Bitmap implements Parcelable {
|
||||
canvas.concat(m);
|
||||
canvas.drawBitmap(source, srcR, dstR, paint);
|
||||
canvas.setBitmap(null);
|
||||
|
||||
// If the source has a gainmap, apply the same set of transformations to the gainmap
|
||||
// and set it on the output
|
||||
if (source.hasGainmap()) {
|
||||
Bitmap newMapContents = transformGainmap(source, m, neww, newh, paint, srcR, dstR,
|
||||
deviceR);
|
||||
if (newMapContents != null) {
|
||||
bitmap.setGainmap(new Gainmap(source.getGainmap(), newMapContents));
|
||||
}
|
||||
}
|
||||
|
||||
if (isHardware) {
|
||||
return bitmap.copy(Config.HARDWARE, false);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private static Bitmap transformGainmap(Bitmap source, Matrix m, int neww, int newh, Paint paint,
|
||||
Rect srcR, RectF dstR, RectF deviceR) {
|
||||
Canvas canvas;
|
||||
Bitmap sourceGainmap = source.getGainmap().getGainmapContents();
|
||||
// Gainmaps can be scaled relative to the base image (eg, 1/4th res)
|
||||
// Preserve that relative scaling between the base & gainmap in the output
|
||||
float scaleX = (sourceGainmap.getWidth() / (float) source.getWidth());
|
||||
float scaleY = (sourceGainmap.getHeight() / (float) source.getHeight());
|
||||
int mapw = Math.round(neww * scaleX);
|
||||
int maph = Math.round(newh * scaleY);
|
||||
|
||||
if (mapw == 0 || maph == 0) {
|
||||
// The gainmap has been scaled away entirely, drop it
|
||||
return null;
|
||||
}
|
||||
|
||||
// Scale the computed `srcR` used for rendering the source bitmap to the destination
|
||||
// to be in gainmap dimensions
|
||||
Rect gSrcR = new Rect((int) (srcR.left * scaleX),
|
||||
(int) (srcR.top * scaleY), (int) (srcR.right * scaleX),
|
||||
(int) (srcR.bottom * scaleY));
|
||||
|
||||
// Note: createBitmap isn't used as that requires a non-null colorspace, however
|
||||
// gainmaps don't have a colorspace. So use `nativeCreate` directly to bypass
|
||||
// that colorspace enforcement requirement (#getColorSpace() allows a null return)
|
||||
Bitmap newMapContents = nativeCreate(null, 0, mapw, mapw, maph,
|
||||
sourceGainmap.getConfig().nativeInt, true, 0);
|
||||
newMapContents.eraseColor(0);
|
||||
canvas = new Canvas(newMapContents);
|
||||
// Scale the translate & matrix to be in gainmap-relative dimensions
|
||||
canvas.scale(scaleX, scaleY);
|
||||
canvas.translate(-deviceR.left, -deviceR.top);
|
||||
canvas.concat(m);
|
||||
canvas.drawBitmap(sourceGainmap, gSrcR, dstR, paint);
|
||||
canvas.setBitmap(null);
|
||||
// Create a new gainmap using a copy of the metadata information from the source but
|
||||
// with the transformed bitmap created above
|
||||
return newMapContents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mutable bitmap with the specified width and height. Its
|
||||
* initial density is as per {@link #getDensity}. The newly created
|
||||
|
||||
@@ -121,6 +121,16 @@ public final class Gainmap implements Parcelable {
|
||||
this(gainmapContents, nCreateEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new gainmap using the provided gainmap as the metadata source and the provided
|
||||
* bitmap as the replacement for the gainmapContents
|
||||
* TODO: Make public, it's useful
|
||||
* @hide
|
||||
*/
|
||||
public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) {
|
||||
this(gainmapContents, nCreateCopy(gainmap.mNativePtr));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns the image data of the gainmap represented as a Bitmap. This is represented
|
||||
* as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored
|
||||
@@ -325,6 +335,7 @@ public final class Gainmap implements Parcelable {
|
||||
|
||||
private static native long nGetFinalizer();
|
||||
private static native long nCreateEmpty();
|
||||
private static native long nCreateCopy(long source);
|
||||
|
||||
private static native void nSetBitmap(long ptr, Bitmap bitmap);
|
||||
|
||||
|
||||
@@ -86,6 +86,16 @@ jlong Gainmap_createEmpty(JNIEnv*, jobject) {
|
||||
return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
|
||||
}
|
||||
|
||||
jlong Gainmap_createCopy(JNIEnv*, jobject, jlong sourcePtr) {
|
||||
Gainmap* gainmap = new Gainmap();
|
||||
gainmap->incStrong(0);
|
||||
if (sourcePtr) {
|
||||
Gainmap* src = fromJava(sourcePtr);
|
||||
gainmap->info = src->info;
|
||||
}
|
||||
return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
|
||||
}
|
||||
|
||||
static void Gainmap_setBitmap(JNIEnv* env, jobject, jlong gainmapPtr, jobject jBitmap) {
|
||||
android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap);
|
||||
fromJava(gainmapPtr)->bitmap = sk_ref_sp(bitmap);
|
||||
@@ -237,6 +247,7 @@ static void Gainmap_readFromParcel(JNIEnv* env, jobject, jlong nativeObject, job
|
||||
static const JNINativeMethod gGainmapMethods[] = {
|
||||
{"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer},
|
||||
{"nCreateEmpty", "()J", (void*)Gainmap_createEmpty},
|
||||
{"nCreateCopy", "(J)J", (void*)Gainmap_createCopy},
|
||||
{"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap},
|
||||
{"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin},
|
||||
{"nGetRatioMin", "(J[F)V", (void*)Gainmap_getRatioMin},
|
||||
|
||||
122
tests/SilkFX/res/layout/gainmap_transform_test.xml
Normal file
122
tests/SilkFX/res/layout/gainmap_transform_test.xml
Normal file
@@ -0,0 +1,122 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2023 The Android Open Source Project
|
||||
~
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<com.android.test.silkfx.hdr.GainmapTransformsTest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/original"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Original" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/scaled"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Scaled (1/3)" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rotate_90"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Rotate 90" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/rotate_90_scaled"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Rot90+Scale" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/crop"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Crop" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/crop_200"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Crop 200" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/source_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sdr_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sdr_source"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitStart" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_weight="1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gainmap_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/gainmap"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitStart" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.android.test.silkfx.hdr.GainmapTransformsTest>
|
||||
@@ -55,7 +55,9 @@ private val AllDemos = listOf(
|
||||
Demo("Color Grid", R.layout.color_grid),
|
||||
Demo("Gradient Sweep", R.layout.gradient_sweep),
|
||||
Demo("Gainmap Image", R.layout.gainmap_image),
|
||||
Demo("Gainmap Decode Test", R.layout.gainmap_decode_test, commonControls = false)
|
||||
Demo("Gainmap Decode Test", R.layout.gainmap_decode_test, commonControls = false),
|
||||
Demo("Gainmap Transform Test", R.layout.gainmap_transform_test,
|
||||
commonControls = false)
|
||||
)),
|
||||
DemoGroup("Materials", listOf(
|
||||
Demo("Glass", GlassActivity::class),
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
package com.android.test.silkfx.hdr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.graphics.Gainmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Button
|
||||
@@ -34,6 +39,25 @@ enum class DecodeMode {
|
||||
CropedSquaredScaled33
|
||||
}
|
||||
|
||||
fun gainmapVisualizer(gainmap: Gainmap): Bitmap {
|
||||
val map = gainmap.gainmapContents
|
||||
val gainmapVisualizer = Bitmap.createBitmap(map.width, map.height,
|
||||
Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(gainmapVisualizer!!)
|
||||
val paint = Paint()
|
||||
paint.colorFilter = ColorMatrixColorFilter(
|
||||
floatArrayOf(
|
||||
0f, 0f, 0f, 1f, 0f,
|
||||
0f, 0f, 0f, 1f, 0f,
|
||||
0f, 0f, 0f, 1f, 0f,
|
||||
0f, 0f, 0f, 0f, 255f
|
||||
)
|
||||
)
|
||||
canvas.drawBitmap(map, 0f, 0f, paint)
|
||||
canvas.setBitmap(null)
|
||||
return gainmapVisualizer
|
||||
}
|
||||
|
||||
class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||
|
||||
private fun decode(mode: DecodeMode) {
|
||||
@@ -71,7 +95,7 @@ class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(c
|
||||
}
|
||||
}
|
||||
|
||||
val gainmapContents = gainmapImage.gainmap!!.gainmapContents!!
|
||||
val gainmapContents = gainmapImage.gainmap?.let { gainmapVisualizer(it) }
|
||||
val sdrBitmap = gainmapImage.also { it.gainmap = null }
|
||||
|
||||
findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap)
|
||||
@@ -80,7 +104,7 @@ class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(c
|
||||
|
||||
findViewById<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
|
||||
findViewById<TextView>(R.id.gainmap_label)!!.text =
|
||||
"Gainmap Size: ${gainmapContents.width}x${gainmapContents.height}"
|
||||
"Gainmap Size: ${gainmapContents?.width ?: 0}x${gainmapContents?.height ?: 0}"
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* 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.android.test.silkfx.hdr
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.Matrix
|
||||
import android.util.AttributeSet
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.android.test.silkfx.R
|
||||
|
||||
class GainmapTransformsTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
|
||||
|
||||
private val sourceImage = loadSample()
|
||||
|
||||
private fun loadSample(): Bitmap {
|
||||
val source = ImageDecoder.createSource(resources.assets,
|
||||
"gainmaps/${context.assets.list("gainmaps")!![0]}")
|
||||
|
||||
return ImageDecoder.decodeBitmap(source) { decoder, info, source ->
|
||||
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
|
||||
}
|
||||
}
|
||||
|
||||
private fun process(transform: (Bitmap) -> Bitmap) {
|
||||
val result = transform(sourceImage)
|
||||
|
||||
val gainmapContents = result.gainmap?.let { gainmapVisualizer(it) }
|
||||
val sdrBitmap = result.also { it.gainmap = null }
|
||||
|
||||
findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap)
|
||||
findViewById<TextView>(R.id.sdr_label)!!.text =
|
||||
"SDR Size: ${sdrBitmap.width}x${sdrBitmap.height}"
|
||||
|
||||
findViewById<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
|
||||
findViewById<TextView>(R.id.gainmap_label)!!.text =
|
||||
"Gainmap Size: ${gainmapContents?.width ?: 0}x${gainmapContents?.height ?: 0}"
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
val sourceInfo = findViewById<TextView>(R.id.source_info)!!
|
||||
sourceInfo.text = "Original size ${sourceImage.width}x${sourceImage.height}"
|
||||
process { it.copy(Bitmap.Config.ARGB_8888, false) }
|
||||
|
||||
findViewById<Button>(R.id.original)!!.setOnClickListener {
|
||||
process { it.copy(Bitmap.Config.ARGB_8888, false) }
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.scaled)!!.setOnClickListener {
|
||||
process { Bitmap.createScaledBitmap(it, it.width / 3, it.height / 3, true) }
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.rotate_90)!!.setOnClickListener {
|
||||
process {
|
||||
val width: Int = it.width
|
||||
val height: Int = it.height
|
||||
|
||||
val m = Matrix()
|
||||
m.setRotate(90.0f, (width / 2).toFloat(), (height / 2).toFloat())
|
||||
Bitmap.createBitmap(it, 0, 0, width, height, m, false)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.rotate_90_scaled)!!.setOnClickListener {
|
||||
process {
|
||||
val width: Int = it.width
|
||||
val height: Int = it.height
|
||||
|
||||
val m = Matrix()
|
||||
m.setRotate(90.0f, (width / 2).toFloat(), (height / 2).toFloat())
|
||||
m.preScale(.3f, .3f)
|
||||
Bitmap.createBitmap(it, 0, 0, width, height, m, false)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.crop)!!.setOnClickListener {
|
||||
process {
|
||||
val width: Int = it.width
|
||||
val height: Int = it.height
|
||||
Bitmap.createBitmap(it, width / 2, height / 2,
|
||||
width / 4, height / 4, null, false)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.crop_200)!!.setOnClickListener {
|
||||
process {
|
||||
val width: Int = it.width
|
||||
val height: Int = it.height
|
||||
|
||||
val m = Matrix()
|
||||
m.setRotate(200.0f, (width / 2).toFloat(), (height / 2).toFloat())
|
||||
Bitmap.createBitmap(it, width / 2, height / 2,
|
||||
width / 4, height / 4, m, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user