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