xref: /aosp_15_r20/frameworks/base/core/tests/coretests/src/android/graphics/PaintTest.java (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 /*
2  * Copyright (C) 2015 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.graphics;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.junit.Assert.assertNotEquals;
22 
23 import android.platform.test.annotations.RequiresFlagsEnabled;
24 import android.platform.test.flag.junit.CheckFlagsRule;
25 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
26 import android.test.InstrumentationTestCase;
27 import android.text.TextUtils;
28 
29 import androidx.test.filters.SmallTest;
30 
31 import com.android.text.flags.Flags;
32 
33 import org.junit.Rule;
34 
35 import java.util.Arrays;
36 import java.util.HashSet;
37 
38 /**
39  * PaintTest tests {@link Paint}.
40  */
41 public class PaintTest extends InstrumentationTestCase {
42     @Rule
43     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
44 
45     private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf";
46 
assertEquals(String message, float[] expected, float[] actual)47     static void assertEquals(String message, float[] expected, float[] actual) {
48         if (expected.length != actual.length) {
49             fail(message + " expected array length:<" + expected.length + "> but was:<"
50                     + actual.length + ">");
51         }
52         for (int i = 0; i < expected.length; ++i) {
53             if (expected[i] != actual[i]) {
54                 fail(message + " expected array element[" +i + "]:<" + expected[i] + ">but was:<"
55                         + actual[i] + ">");
56             }
57         }
58     }
59 
60     static class HintingTestCase {
61         public final String mText;
62         public final float mTextSize;
63         public final float[] mWidthWithoutHinting;
64         public final float[] mWidthWithHinting;
65 
HintingTestCase(String text, float textSize, float[] widthWithoutHinting, float[] widthWithHinting)66         public HintingTestCase(String text, float textSize, float[] widthWithoutHinting,
67                                float[] widthWithHinting) {
68             mText = text;
69             mTextSize = textSize;
70             mWidthWithoutHinting = widthWithoutHinting;
71             mWidthWithHinting = widthWithHinting;
72         }
73     }
74 
75     // Following test cases are only valid for HintedAdvanceWidthTest-Regular.ttf in assets/fonts.
76     HintingTestCase[] HINTING_TESTCASES = {
77         new HintingTestCase("H", 11f, new float[] { 7f }, new float[] { 13f }),
78         new HintingTestCase("O", 11f, new float[] { 7f }, new float[] { 13f }),
79 
80         new HintingTestCase("H", 13f, new float[] { 8f }, new float[] { 14f }),
81         new HintingTestCase("O", 13f, new float[] { 9f }, new float[] { 15f }),
82 
83         new HintingTestCase("HO", 11f, new float[] { 7f, 7f }, new float[] { 13f, 13f }),
84         new HintingTestCase("OH", 11f, new float[] { 7f, 7f }, new float[] { 13f, 13f }),
85 
86         new HintingTestCase("HO", 13f, new float[] { 8f, 9f }, new float[] { 14f, 15f }),
87         new HintingTestCase("OH", 13f, new float[] { 9f, 8f }, new float[] { 15f, 14f }),
88     };
89 
90     @SmallTest
testHintingWidth()91     public void testHintingWidth() {
92         final Typeface fontTypeface = Typeface.createFromAsset(
93                 getInstrumentation().getContext().getAssets(), FONT_PATH);
94         Paint paint = new Paint();
95         paint.setTypeface(fontTypeface);
96 
97         for (int i = 0; i < HINTING_TESTCASES.length; ++i) {
98             HintingTestCase testCase = HINTING_TESTCASES[i];
99 
100             paint.setTextSize(testCase.mTextSize);
101 
102             float[] widths = new float[testCase.mText.length()];
103 
104             paint.setHinting(Paint.HINTING_OFF);
105             paint.getTextWidths(String.valueOf(testCase.mText), widths);
106             assertEquals("Text width of '" + testCase.mText + "' without hinting is not expected.",
107                     testCase.mWidthWithoutHinting, widths);
108 
109             paint.setHinting(Paint.HINTING_ON);
110             paint.getTextWidths(String.valueOf(testCase.mText), widths);
111             assertEquals("Text width of '" + testCase.mText + "' with hinting is not expected.",
112                     testCase.mWidthWithHinting, widths);
113         }
114     }
115 
116     private static class HasGlyphTestCase {
117         public final int mBaseCodepoint;
118         public final HashSet<Integer> mVariationSelectors;
119 
HasGlyphTestCase(int baseCodepoint, Integer[] variationSelectors)120         public HasGlyphTestCase(int baseCodepoint, Integer[] variationSelectors) {
121             mBaseCodepoint = baseCodepoint;
122             mVariationSelectors = new HashSet<>(Arrays.asList(variationSelectors));
123         }
124     }
125 
codePointsToString(int[] codepoints)126     private static String codePointsToString(int[] codepoints) {
127         StringBuilder sb = new StringBuilder();
128         for (int codepoint : codepoints) {
129             sb.append(Character.toChars(codepoint));
130         }
131         return sb.toString();
132     }
133 
testHasGlyph_variationSelectors()134     public void testHasGlyph_variationSelectors() {
135         final Typeface fontTypeface = Typeface.createFromAsset(
136                 getInstrumentation().getContext().getAssets(), "fonts/hasGlyphTestFont.ttf");
137         Paint p = new Paint();
138         p.setTypeface(fontTypeface);
139 
140         // Usually latin letters U+0061..U+0064 and Mahjong Tiles U+1F000..U+1F003 don't have
141         // variation selectors.  This test may fail if system pre-installed fonts have a variation
142         // selector support for U+0061..U+0064 and U+1F000..U+1F003.
143         HasGlyphTestCase[] HAS_GLYPH_TEST_CASES = {
144             new HasGlyphTestCase(0x0061, new Integer[] {0xFE00, 0xE0100, 0xE0101, 0xE0102}),
145             new HasGlyphTestCase(0x0062, new Integer[] {0xFE01, 0xE0101, 0xE0102, 0xE0103}),
146             new HasGlyphTestCase(0x0063, new Integer[] {}),
147             new HasGlyphTestCase(0x0064, new Integer[] {0xFE02, 0xE0102, 0xE0103}),
148 
149             new HasGlyphTestCase(0x1F000, new Integer[] {0xFE00, 0xE0100, 0xE0101, 0xE0102}),
150             new HasGlyphTestCase(0x1F001, new Integer[] {0xFE01, 0xE0101, 0xE0102, 0xE0103}),
151             new HasGlyphTestCase(0x1F002, new Integer[] {}),
152             new HasGlyphTestCase(0x1F003, new Integer[] {0xFE02, 0xE0102, 0xE0103}),
153         };
154 
155         for (HasGlyphTestCase testCase : HAS_GLYPH_TEST_CASES) {
156             for (int vs = 0xFE00; vs <= 0xE01EF; ++vs) {
157                 // Move to variation selector supplements after variation selectors.
158                 if (vs == 0xFF00) {
159                     vs = 0xE0100;
160                 }
161                 final String signature =
162                         "hasGlyph(U+" + Integer.toHexString(testCase.mBaseCodepoint) +
163                         " U+" + Integer.toHexString(vs) + ")";
164                 final String testString =
165                         codePointsToString(new int[] {testCase.mBaseCodepoint, vs});
166                 if (vs == 0xFE0E // U+FE0E is the text presentation emoji. hasGlyph is expected to
167                                  // return true for that variation selector if the font has the base
168                                  // glyph.
169                              || testCase.mVariationSelectors.contains(vs)) {
170                     assertTrue(signature + " is expected to be true", p.hasGlyph(testString));
171                 } else {
172                     assertFalse(signature + " is expected to be false", p.hasGlyph(testString));
173                 }
174             }
175         }
176     }
177 
testGetTextRunAdvances()178     public void testGetTextRunAdvances() {
179         {
180             // LTR
181             String text = "abcdef";
182             assertGetTextRunAdvances(text, 0, text.length(), 0, text.length(), false, true);
183             assertGetTextRunAdvances(text, 1, text.length() - 1, 0, text.length(), false, false);
184         }
185         {
186             // RTL
187             final String text =
188                     "\u0645\u0627\u0020\u0647\u064A\u0020\u0627\u0644\u0634" +
189                             "\u0641\u0631\u0629\u0020\u0627\u0644\u0645\u0648\u062D" +
190                             "\u062F\u0629\u0020\u064A\u0648\u0646\u064A\u0643\u0648" +
191                             "\u062F\u061F";
192             assertGetTextRunAdvances(text, 0, text.length(), 0, text.length(), true, true);
193             assertGetTextRunAdvances(text, 1, text.length() - 1, 0, text.length(), true, false);
194         }
195     }
196 
assertGetTextRunAdvances(String str, int start, int end, int contextStart, int contextEnd, boolean isRtl, boolean compareWithOtherMethods)197     private void assertGetTextRunAdvances(String str, int start, int end,
198             int contextStart, int contextEnd, boolean isRtl, boolean compareWithOtherMethods) {
199         Paint p = new Paint();
200 
201         final int count = end - start;
202         final int contextCount = contextEnd - contextStart;
203         final float[][] advanceArrays = new float[2][count];
204         char chars[] = str.toCharArray();
205         final float advance = p.getTextRunAdvances(chars, start, count,
206                 contextStart, contextCount, isRtl, advanceArrays[0], 0);
207         for (int c = 1; c < count; ++c) {
208             final float firstPartAdvance = p.getTextRunAdvances(chars, start, c,
209                     contextStart, contextCount, isRtl, advanceArrays[1], 0);
210             final float secondPartAdvance = p.getTextRunAdvances(chars, start + c, count - c,
211                     contextStart, contextCount, isRtl, advanceArrays[1], c);
212             assertEquals(advance, firstPartAdvance + secondPartAdvance, 1.0f);
213 
214             for (int j = 0; j < count; j++) {
215                 assertEquals(advanceArrays[0][j], advanceArrays[1][j], 1.0f);
216             }
217 
218 
219             // Compare results with measureText, getRunAdvance, and getTextWidths.
220             if (compareWithOtherMethods && start == contextStart && end == contextEnd) {
221                 assertEquals(advance, p.measureText(str, start, end), 1.0f);
222                 assertEquals(advance, p.getRunAdvance(
223                         chars, start, count, contextStart, contextCount, isRtl, end), 1.0f);
224 
225                 final float[] widths = new float[count];
226                 p.getTextWidths(str, start, end, widths);
227                 for (int i = 0; i < count; i++) {
228                     assertEquals(advanceArrays[0][i], widths[i], 1.0f);
229                 }
230             }
231         }
232     }
233 
testGetTextRunAdvances_invalid()234     public void testGetTextRunAdvances_invalid() {
235         Paint p = new Paint();
236         char[] text = "test".toCharArray();
237 
238         try {
239             p.getTextRunAdvances((char[])null, 0, 0, 0, 0, false, null, 0);
240             fail("Should throw an IllegalArgumentException.");
241         } catch (IllegalArgumentException e) {
242         }
243 
244         try {
245             p.getTextRunAdvances(text, 0, text.length, 0, text.length, false,
246                     new float[text.length - 1], 0);
247             fail("Should throw an IndexOutOfBoundsException.");
248         } catch (IndexOutOfBoundsException e) {
249         }
250 
251         try {
252             p.getTextRunAdvances(text, 0, text.length, 0, text.length, false,
253                     new float[text.length], 1);
254             fail("Should throw an IndexOutOfBoundsException.");
255         } catch (IndexOutOfBoundsException e) {
256         }
257 
258         // 0 > contextStart
259         try {
260             p.getTextRunAdvances(text, 0, text.length, -1, text.length, false, null, 0);
261             fail("Should throw an IndexOutOfBoundsException.");
262         } catch (IndexOutOfBoundsException e) {
263         }
264 
265         // contextStart > start
266         try {
267             p.getTextRunAdvances(text, 0, text.length, 1, text.length, false, null, 0);
268             fail("Should throw an IndexOutOfBoundsException.");
269         } catch (IndexOutOfBoundsException e) {
270         }
271 
272         // end > contextEnd
273         try {
274             p.getTextRunAdvances(text, 0, text.length, 0, text.length - 1, false, null, 0);
275             fail("Should throw an IndexOutOfBoundsException.");
276         } catch (IndexOutOfBoundsException e) {
277         }
278 
279         // contextEnd > text.length
280         try {
281             p.getTextRunAdvances(text, 0, text.length, 0, text.length + 1, false, null, 0);
282             fail("Should throw an IndexOutOfBoundsException.");
283         } catch (IndexOutOfBoundsException e) {
284         }
285     }
286 
testMeasureTextBidi()287     public void testMeasureTextBidi() {
288         Paint p = new Paint();
289         {
290             String bidiText = "abc \u0644\u063A\u0629 def";
291             p.setBidiFlags(Paint.BIDI_LTR);
292             float width = p.measureText(bidiText, 0, 4);
293             p.setBidiFlags(Paint.BIDI_RTL);
294             width += p.measureText(bidiText, 4, 7);
295             p.setBidiFlags(Paint.BIDI_LTR);
296             width += p.measureText(bidiText, 7, bidiText.length());
297             assertEquals(width, p.measureText(bidiText), 1.0f);
298         }
299         {
300             String bidiText = "abc \u0644\u063A\u0629 def";
301             p.setBidiFlags(Paint.BIDI_DEFAULT_LTR);
302             float width = p.measureText(bidiText, 0, 4);
303             width += p.measureText(bidiText, 4, 7);
304             width += p.measureText(bidiText, 7, bidiText.length());
305             assertEquals(width, p.measureText(bidiText), 1.0f);
306         }
307         {
308             String bidiText = "abc \u0644\u063A\u0629 def";
309             p.setBidiFlags(Paint.BIDI_FORCE_LTR);
310             float width = p.measureText(bidiText, 0, 4);
311             width += p.measureText(bidiText, 4, 7);
312             width += p.measureText(bidiText, 7, bidiText.length());
313             assertEquals(width, p.measureText(bidiText), 1.0f);
314         }
315         {
316             String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
317             p.setBidiFlags(Paint.BIDI_RTL);
318             float width = p.measureText(bidiText, 0, 4);
319             p.setBidiFlags(Paint.BIDI_LTR);
320             width += p.measureText(bidiText, 4, 7);
321             p.setBidiFlags(Paint.BIDI_RTL);
322             width += p.measureText(bidiText, 7, bidiText.length());
323             assertEquals(width, p.measureText(bidiText), 1.0f);
324         }
325         {
326             String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
327             p.setBidiFlags(Paint.BIDI_DEFAULT_RTL);
328             float width = p.measureText(bidiText, 0, 4);
329             width += p.measureText(bidiText, 4, 7);
330             width += p.measureText(bidiText, 7, bidiText.length());
331             assertEquals(width, p.measureText(bidiText), 1.0f);
332         }
333         {
334             String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
335             p.setBidiFlags(Paint.BIDI_FORCE_RTL);
336             float width = p.measureText(bidiText, 0, 4);
337             width += p.measureText(bidiText, 4, 7);
338             width += p.measureText(bidiText, 7, bidiText.length());
339             assertEquals(width, p.measureText(bidiText), 1.0f);
340         }
341     }
342 
testSetGetWordSpacing()343     public void testSetGetWordSpacing() {
344         Paint p = new Paint();
345         assertEquals(0.0f, p.getWordSpacing());  // The default value should be 0.
346         p.setWordSpacing(1.0f);
347         assertEquals(1.0f, p.getWordSpacing());
348         p.setWordSpacing(-2.0f);
349         assertEquals(-2.0f, p.getWordSpacing());
350     }
351 
testGetUnderlinePositionAndThickness()352     public void testGetUnderlinePositionAndThickness() {
353         final Typeface fontTypeface = Typeface.createFromAsset(
354                 getInstrumentation().getContext().getAssets(), "fonts/underlineTestFont.ttf");
355         final Paint p = new Paint();
356         final int textSize = 100;
357         p.setTextSize(textSize);
358 
359         final float origPosition = p.getUnderlinePosition();
360         final float origThickness = p.getUnderlineThickness();
361 
362         p.setTypeface(fontTypeface);
363         assertNotEquals(origPosition, p.getUnderlinePosition());
364         assertNotEquals(origThickness, p.getUnderlineThickness());
365 
366         //    -200 (underlinePosition in 'post' table, negative means below the baseline)
367         //    ÷ 1000 (unitsPerEm in 'head' table)
368         //    × 100 (text size)
369         //    × -1 (negated, since we consider below the baseline positive)
370         //    = 20
371         assertEquals(20.0f, p.getUnderlinePosition(), 0.5f);
372         //    300 (underlineThickness in 'post' table)
373         //    ÷ 1000 (unitsPerEm in 'head' table)
374         //    × 100 (text size)
375         //    = 30
376         assertEquals(30.0f, p.getUnderlineThickness(), 0.5f);
377     }
378 
getClusterCount(Paint p, String text)379     private int getClusterCount(Paint p, String text) {
380         Paint.RunInfo runInfo = new Paint.RunInfo();
381         p.getRunCharacterAdvance(text, 0, text.length(), 0, text.length(), false, 0, null, 0, null,
382                 runInfo);
383         int ccByString = runInfo.getClusterCount();
384         runInfo.setClusterCount(0);
385         char[] buf = new char[text.length()];
386         TextUtils.getChars(text, 0, text.length(), buf, 0);
387         p.getRunCharacterAdvance(buf, 0, buf.length, 0, buf.length, false, 0, null, 0, null,
388                 runInfo);
389         int ccByChars = runInfo.getClusterCount();
390         assertEquals(ccByChars, ccByString);
391         return ccByChars;
392     }
393 
testCluster()394     public void testCluster() {
395         final Paint p = new Paint();
396         p.setTextSize(100);
397 
398         // Regular String
399         assertEquals(1, getClusterCount(p, "A"));
400         assertEquals(2, getClusterCount(p, "AB"));
401 
402         // Ligature is in the same cluster
403         assertEquals(1, getClusterCount(p, "fi"));  // Ligature
404         p.setFontFeatureSettings("'liga' off");
405         assertEquals(2, getClusterCount(p, "fi"));  // Ligature is disabled
406         p.setFontFeatureSettings("");
407 
408         // Combining character
409         assertEquals(1, getClusterCount(p, "\u0061\u0300"));  // A + COMBINING GRAVE ACCENT
410 
411         // BiDi
412         final String rtlStr = "\u05D0\u05D1\u05D2";
413         final String ltrStr = "abc";
414         assertEquals(3, getClusterCount(p, rtlStr));
415         assertEquals(6, getClusterCount(p, rtlStr + ltrStr));
416         assertEquals(9, getClusterCount(p, ltrStr + rtlStr + ltrStr));
417     }
418 
419     @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
testDerivedFromSameTypeface()420     public void testDerivedFromSameTypeface() {
421         final Paint p = new Paint();
422 
423         p.setTypeface(Typeface.SANS_SERIF);
424         assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
425         Typeface first = p.getTypeface();
426 
427         p.setTypeface(Typeface.SANS_SERIF);
428         assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
429         Typeface second = p.getTypeface();
430 
431         assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
432     }
433 
434     @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS)
testDerivedFromChained()435     public void testDerivedFromChained() {
436         final Paint p = new Paint();
437 
438         p.setTypeface(Typeface.SANS_SERIF);
439         assertThat(p.setFontVariationSettings("'wght' 450")).isTrue();
440         Typeface first = p.getTypeface();
441 
442         assertThat(p.setFontVariationSettings("'wght' 480")).isTrue();
443         Typeface second = p.getTypeface();
444 
445         assertThat(first.getDerivedFrom()).isSameInstanceAs(second.getDerivedFrom());
446     }
447 }
448