1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.uirendering.cts.testclasses; 18 19 import static android.graphics.Bitmap.Config.ARGB_8888; 20 import static android.graphics.Bitmap.Config.HARDWARE; 21 import static android.graphics.Bitmap.Config.RGB_565; 22 23 import android.content.res.AssetManager; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.BitmapShader; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.ColorSpace; 30 import android.graphics.HardwareBufferRenderer; 31 import android.graphics.Paint; 32 import android.graphics.Point; 33 import android.graphics.RecordingCanvas; 34 import android.graphics.RenderNode; 35 import android.graphics.Shader; 36 import android.graphics.fonts.Font; 37 import android.graphics.fonts.SystemFonts; 38 import android.hardware.HardwareBuffer; 39 import android.uirendering.cts.bitmapverifiers.SamplePointVerifier; 40 import android.uirendering.cts.testinfrastructure.ActivityTestBase; 41 import android.uirendering.cts.util.BitmapDumper; 42 43 import androidx.annotation.ColorLong; 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.test.filters.MediumTest; 47 import androidx.test.runner.AndroidJUnit4; 48 49 import org.junit.Assert; 50 import org.junit.Before; 51 import org.junit.Test; 52 import org.junit.runner.RunWith; 53 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.util.concurrent.CountDownLatch; 57 import java.util.concurrent.TimeUnit; 58 59 @MediumTest 60 @RunWith(AndroidJUnit4.class) 61 public class ColorSpaceTests extends ActivityTestBase { 62 private Bitmap mMask; 63 64 @Before loadMask()65 public void loadMask() { 66 Bitmap res = BitmapFactory.decodeResource(getActivity().getResources(), 67 android.uirendering.cts.R.drawable.alpha_mask); 68 mMask = Bitmap.createBitmap(res.getWidth(), res.getHeight(), Bitmap.Config.ALPHA_8); 69 Canvas c = new Canvas(mMask); 70 c.drawBitmap(res, 0, 0, null); 71 } 72 73 @Test testDrawDisplayP3()74 public void testDrawDisplayP3() { 75 // Uses hardware transfer function 76 Bitmap bitmap8888 = loadAsset("green-p3.png", ARGB_8888); 77 Bitmap bitmapHardware = loadAsset("green-p3.png", HARDWARE); 78 createTest() 79 .addCanvasClient("Draw_DisplayP3_8888", 80 (c, w, h) -> drawAsset(c, bitmap8888), true) 81 .addCanvasClient( 82 (c, w, h) -> drawAsset(c, bitmapHardware), true) 83 .runWithVerifier(new SamplePointVerifier( 84 new Point[] { 85 point(0, 0), point(48, 0), point(32, 40), point(0, 40), point(0, 56) 86 }, 87 new int[] { 0xff00ff00, 0xff00ff00, 0xff00ff00, 0xffffffff, 0xff7f7f00 } 88 )); 89 } 90 91 @Test testDrawDisplayP3Config565()92 public void testDrawDisplayP3Config565() { 93 // Uses hardware transfer function 94 Bitmap bitmap = loadAsset("green-p3.png", RGB_565); 95 createTest() 96 .addCanvasClient("Draw_DisplayP3_565", (c, w, h) -> drawAsset(c, bitmap), true) 97 .runWithVerifier(new SamplePointVerifier( 98 new Point[] { 99 point(0, 0), point(48, 0), point(32, 40), point(0, 40), point(0, 56) 100 }, 101 new int[] { 0xff00ff00, 0xff00ff00, 0xff00ff00, 0xffffffff, 0xff7f7f00 } 102 )); 103 } 104 105 @Test testDrawProPhotoRGB()106 public void testDrawProPhotoRGB() { 107 // Uses hardware limited shader transfer function 108 Bitmap bitmap8888 = loadAsset("orange-prophotorgb.png", ARGB_8888); 109 Bitmap bitmapHardware = loadAsset("orange-prophotorgb.png", HARDWARE); 110 createTest() 111 .addCanvasClient("Draw_ProPhotoRGB_8888", 112 (c, w, h) -> drawAsset(c, bitmap8888), true) 113 .addCanvasClient( 114 (c, w, h) -> drawAsset(c, bitmapHardware), true) 115 .runWithVerifier(new SamplePointVerifier( 116 new Point[] { 117 point(0, 0), point(48, 0), point(32, 40), point(0, 40), point(0, 56) 118 }, 119 new int[] { 0xffff7f00, 0xffff7f00, 0xffff7f00, 0xffffffff, 0xffff3f00 } 120 )); 121 } 122 123 @Test testDrawProPhotoRGBConfig565()124 public void testDrawProPhotoRGBConfig565() { 125 // Uses hardware limited shader transfer function 126 Bitmap bitmap = loadAsset("orange-prophotorgb.png", RGB_565); 127 createTest() 128 .addCanvasClient("Draw_ProPhotoRGB_565", 129 (c, w, h) -> drawAsset(c, bitmap), true) 130 .runWithVerifier(new SamplePointVerifier( 131 new Point[] { 132 point(0, 0), point(48, 0), point(32, 40), point(0, 40), point(0, 56) 133 }, 134 new int[] { 0xffff7f00, 0xffff7f00, 0xffff7f00, 0xffffffff, 0xffff3f00 } 135 )); 136 } 137 138 @Test testDrawTranslucentAdobeRGB()139 public void testDrawTranslucentAdobeRGB() { 140 // Uses hardware simplified gamma transfer function 141 Bitmap bitmap8888 = loadAsset("red-adobergb.png", ARGB_8888); 142 Bitmap bitmapHardware = loadAsset("red-adobergb.png", HARDWARE); 143 createTest() 144 .addCanvasClient("Draw_AdobeRGB_Translucent_8888", 145 (c, w, h) -> drawTranslucentAsset(c, bitmap8888), true) 146 .addCanvasClient( 147 (c, w, h) -> drawTranslucentAsset(c, bitmapHardware), true) 148 .runWithVerifier(new SamplePointVerifier( 149 new Point[] { point(0, 0) }, 150 new int[] { 0xffed8080 } 151 )); 152 } 153 154 @Test testHlgWhitePoint()155 public void testHlgWhitePoint() { 156 final ColorSpace bt2020_hlg = ColorSpace.get(ColorSpace.Named.BT2020_HLG); 157 ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.SRGB), 158 bt2020_hlg); 159 float[] connectorResult = connector.transform(1f, 1f, 1f); 160 Assert.assertEquals(.75f, connectorResult[0], 0.001f); 161 Assert.assertEquals(.75f, connectorResult[1], 0.001f); 162 Assert.assertEquals(.75f, connectorResult[2], 0.001f); 163 164 Color bitmapResult = transformViaBitmap(Color.pack(Color.WHITE), bt2020_hlg); 165 Assert.assertEquals(.75f, bitmapResult.red(), 0.001f); 166 Assert.assertEquals(.75f, bitmapResult.green(), 0.001f); 167 Assert.assertEquals(.75f, bitmapResult.blue(), 0.001f); 168 } 169 170 @Test testPqWhitePoint()171 public void testPqWhitePoint() { 172 final ColorSpace bt2020_pq = ColorSpace.get(ColorSpace.Named.BT2020_PQ); 173 ColorSpace.Connector connector = ColorSpace.connect(ColorSpace.get(ColorSpace.Named.SRGB), 174 bt2020_pq); 175 float[] connectorResult = connector.transform(1f, 1f, 1f); 176 Assert.assertEquals(.58f, connectorResult[0], 0.001f); 177 Assert.assertEquals(.58f, connectorResult[1], 0.001f); 178 Assert.assertEquals(.58f, connectorResult[2], 0.001f); 179 180 Color bitmapResult = transformViaBitmap(Color.pack(Color.WHITE), bt2020_pq); 181 Assert.assertEquals(.58f, bitmapResult.red(), 0.001f); 182 Assert.assertEquals(.58f, bitmapResult.green(), 0.001f); 183 Assert.assertEquals(.58f, bitmapResult.blue(), 0.001f); 184 } 185 186 @Test testEmojiRespectsColorSpace()187 public void testEmojiRespectsColorSpace() { 188 HardwareBuffer buffer = HardwareBuffer.create(32, 32, HardwareBuffer.RGBA_8888, 189 1, HardwareBuffer.USAGE_GPU_COLOR_OUTPUT | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); 190 final ColorSpace dest = ColorSpace.get(ColorSpace.Named.BT2020_PQ); 191 HardwareBufferRenderer renderer = new HardwareBufferRenderer(buffer); 192 RenderNode content = new RenderNode("emoji"); 193 content.setPosition(0, 0, 32, 32); 194 RecordingCanvas canvas = content.beginRecording(); 195 Paint p = new Paint(); 196 p.setTextSize(32); 197 canvas.drawColor(Color.pack(1.0f, 1.0f, 1.0f, 1.0f, dest)); 198 canvas.drawText(Character.toString('\u2B1C'), 0.0f, 32.0f, p); 199 content.endRecording(); 200 renderer.setContentRoot(content); 201 CountDownLatch latch = new CountDownLatch(1); 202 renderer.obtainRenderRequest().setColorSpace(dest).draw(Runnable::run, result -> { 203 result.getFence().awaitForever(); 204 latch.countDown(); 205 }); 206 try { 207 Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); 208 } catch (InterruptedException ex) { 209 Assert.fail(ex.getMessage()); 210 } 211 Bitmap result = Bitmap.wrapHardwareBuffer(buffer, dest) 212 .copy(Bitmap.Config.ARGB_8888, false); 213 Color color = result.getColor(16, 16).convert( 214 ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); 215 if (color.red() > 1 || color.blue() > 1 || color.green() > 1) { 216 BitmapDumper.dumpBitmap(result); 217 Assert.fail("Emoji failed colorspace conversion; got " + color.red() + ", " 218 + color.blue() + ", " + color.green()); 219 } 220 } 221 222 // Renders many glyphs from a color font to overflow into Skia's multi-atlas codepath. 223 // 224 // Originally created to ensure SkSL helper functions (for e.g. colorspace conversion) aren't 225 // duplicated when needing to pull from multiple atlases, which could cause a shader compilation 226 // error resulting in no glyphs being drawn. 227 @Test testMultiAtlasGlyphsWithColorSpace()228 public void testMultiAtlasGlyphsWithColorSpace() throws IOException { 229 final int canvasSize = 64; 230 final int[] textSizes = { 80, 60, 40, 30, 25, 20, 18, 13, 12, 11, 10, 9, 8 }; 231 final int numGlyphs = 1000; 232 final int[] glyphIds = new int[numGlyphs]; 233 final float[] positions = new float[2 * numGlyphs]; 234 for (int i = 0; i < numGlyphs; i++) { 235 glyphIds[i] = i; 236 // Position in bottom left to better fill space 237 positions[2 * i] = 0; 238 positions[2 * i + 1] = canvasSize; 239 } 240 241 Font font = null; 242 for (Font sysFont : SystemFonts.getAvailableFonts()) { 243 if (sysFont.getFile().getName().equals("NotoColorEmoji.ttf")) { 244 font = sysFont; 245 break; 246 } 247 } 248 // Per SystemEmojiTest#uniquePostScript (CtsGraphicsTestCases), NotoColorEmoji.ttf should 249 // always be available as a fallback font, even if other emoji font files are installed on 250 // the system. 251 Assert.assertNotNull(font); 252 253 HardwareBuffer buffer = HardwareBuffer.create(canvasSize, canvasSize, 254 HardwareBuffer.RGBA_8888, 1, 255 HardwareBuffer.USAGE_GPU_COLOR_OUTPUT | HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE); 256 final ColorSpace dest = ColorSpace.get(ColorSpace.Named.BT2020_PQ); // Colorspace conversion 257 HardwareBufferRenderer renderer = new HardwareBufferRenderer(buffer); 258 RenderNode content = new RenderNode("colored_glyphs"); 259 content.setPosition(0, 0, canvasSize, canvasSize); 260 Paint p = new Paint(); 261 262 // Render twice to ensure the final image was all rendered after the switch to multi-atlas 263 for (int renderAttempt = 0; renderAttempt < 2; renderAttempt++) { 264 RecordingCanvas canvas = content.beginRecording(); 265 // Start with a white background 266 canvas.drawColor(Color.pack(1.0f, 1.0f, 1.0f, 1.0f, dest)); 267 268 for (int i = 0; i < textSizes.length; i++) { 269 p.setTextSize(textSizes[i]); 270 canvas.drawGlyphs(glyphIds, 0, positions, 0, glyphIds.length, font, p); 271 } 272 273 content.endRecording(); 274 renderer.setContentRoot(content); 275 CountDownLatch latch = new CountDownLatch(1); 276 renderer.obtainRenderRequest().setColorSpace(dest).draw(Runnable::run, result -> { 277 result.getFence().awaitForever(); 278 latch.countDown(); 279 }); 280 try { 281 Assert.assertTrue(latch.await(60, TimeUnit.SECONDS)); 282 } catch (InterruptedException ex) { 283 Assert.fail(ex.getMessage()); 284 } 285 } 286 Bitmap result = Bitmap.wrapHardwareBuffer(buffer, dest) 287 .copy(Bitmap.Config.ARGB_8888, false); 288 289 // Ensure that some pixels are neither white nor black. The emoji include other colors, and 290 // if we only see white and black (or gray), then they are not being rendered correctly. 291 // (Some glyph shapes may render black even on failure.) 292 final float saturationThreshold = 0.01f; 293 for (int y = 0; y < canvasSize; y++) { 294 for (int x = 0; x < canvasSize; x++) { 295 Color color = result.getColor(x, y); 296 float[] hsv = new float[3]; 297 Color.colorToHSV(color.toArgb(), hsv); 298 if (hsv[1] > saturationThreshold) { 299 // Success! 300 return; 301 } 302 } 303 } 304 // All pixels failed 305 BitmapDumper.dumpBitmap(result); 306 Assert.fail("Failed to render render glyphs from multiple atlases while a colorspace" 307 + " conversion was set. All pixels were either white or black."); 308 } 309 drawAsset(@onNull Canvas canvas, Bitmap bitmap)310 private void drawAsset(@NonNull Canvas canvas, Bitmap bitmap) { 311 // Render bitmap directly 312 canvas.save(); 313 canvas.clipRect(0, 0, 32, 32); 314 canvas.drawBitmap(bitmap, 0, 0, null); 315 canvas.restore(); 316 317 // Render bitmap via shader 318 Paint p = new Paint(); 319 p.setShader(new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); 320 canvas.drawRect(32.0f, 0.0f, 64.0f, 32.0f, p); 321 322 // Render bitmap via shader using another bitmap as a mask 323 canvas.save(); 324 canvas.clipRect(0, 32, 64, 48); 325 canvas.drawBitmap(mMask, 0, 0, p); 326 canvas.restore(); 327 328 // Render bitmap with alpha to test modulation 329 p.setShader(null); 330 p.setAlpha(127); 331 canvas.save(); 332 canvas.clipRect(0, 48, 64, 64); 333 canvas.drawColor(0xffff0000); 334 canvas.drawBitmap(bitmap, 0, 0, p); 335 canvas.restore(); 336 } 337 338 @Nullable loadAsset(@onNull String assetName, @NonNull Bitmap.Config config)339 private Bitmap loadAsset(@NonNull String assetName, @NonNull Bitmap.Config config) { 340 Bitmap bitmap; 341 AssetManager assets = getActivity().getResources().getAssets(); 342 try (InputStream in = assets.open(assetName)) { 343 BitmapFactory.Options opts = new BitmapFactory.Options(); 344 opts.inPreferredConfig = config; 345 346 bitmap = BitmapFactory.decodeStream(in, null, opts); 347 } catch (IOException e) { 348 throw new RuntimeException("Test failed: ", e); 349 } 350 return bitmap; 351 } 352 drawTranslucentAsset(@onNull Canvas canvas, Bitmap bitmap)353 private void drawTranslucentAsset(@NonNull Canvas canvas, Bitmap bitmap) { 354 canvas.drawBitmap(bitmap, 0, 0, null); 355 } 356 357 @NonNull point(int x, int y)358 private static Point point(int x, int y) { 359 return new Point(x, y); 360 } 361 transformViaBitmap(@olorLong long source, ColorSpace dest)362 private static Color transformViaBitmap(@ColorLong long source, ColorSpace dest) { 363 Bitmap bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.RGBA_F16, false, dest); 364 Canvas canvas = new Canvas(bitmap); 365 canvas.drawColor(source); 366 return bitmap.getColor(0, 0); 367 } 368 } 369