xref: /aosp_15_r20/cts/tests/tests/uirendering/src/android/uirendering/cts/testclasses/ColorSpaceTests.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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