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
17package android.graphics;
18
19import static org.junit.Assert.assertNotEquals;
20
21import android.graphics.Paint;
22import android.test.InstrumentationTestCase;
23import android.test.suitebuilder.annotation.SmallTest;
24
25import java.util.Arrays;
26import java.util.HashSet;
27
28/**
29 * PaintTest tests {@link Paint}.
30 */
31public class PaintTest extends InstrumentationTestCase {
32    private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf";
33
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
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
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
107        public HasGlyphTestCase(int baseCodepoint, Integer[] variationSelectors) {
108            mBaseCodepoint = baseCodepoint;
109            mVariationSelectors = new HashSet<>(Arrays.asList(variationSelectors));
110        }
111    }
112
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
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
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
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 float[][] advanceArrays = new float[4][count];
190
191        final float advance = p.getTextRunAdvances(str, start, end, contextStart, contextEnd,
192                isRtl, advanceArrays[0], 0);
193
194        char chars[] = str.toCharArray();
195        final float advance_c = p.getTextRunAdvances(chars, start, count, contextStart,
196                contextEnd - contextStart, isRtl, advanceArrays[1], 0);
197        assertEquals(advance, advance_c, 1.0f);
198
199        for (int c = 1; c < count; ++c) {
200            final float firstPartAdvance = p.getTextRunAdvances(str, start, start + c,
201                    contextStart, contextEnd, isRtl, advanceArrays[2], 0);
202            final float secondPartAdvance = p.getTextRunAdvances(str, start + c, end,
203                    contextStart, contextEnd, isRtl, advanceArrays[2], c);
204            assertEquals(advance, firstPartAdvance + secondPartAdvance, 1.0f);
205
206
207            final float firstPartAdvance_c = p.getTextRunAdvances(chars, start, c,
208                    contextStart, contextEnd - contextStart, isRtl, advanceArrays[3], 0);
209            final float secondPartAdvance_c = p.getTextRunAdvances(chars, start + c,
210                    count - c, contextStart, contextEnd - contextStart, isRtl,
211                    advanceArrays[3], c);
212            assertEquals(advance, firstPartAdvance_c + secondPartAdvance_c, 1.0f);
213            assertEquals(firstPartAdvance, firstPartAdvance_c, 1.0f);
214            assertEquals(secondPartAdvance, secondPartAdvance_c, 1.0f);
215
216            for (int i = 1; i < advanceArrays.length; i++) {
217                for (int j = 0; j < count; j++) {
218                    assertEquals(advanceArrays[0][j], advanceArrays[i][j], 1.0f);
219                }
220            }
221
222            // Compare results with measureText, getRunAdvance, and getTextWidths.
223            if (compareWithOtherMethods && start == contextStart && end == contextEnd) {
224                assertEquals(advance, p.measureText(str, start, end), 1.0f);
225                assertEquals(advance, p.getRunAdvance(
226                        str, start, end, contextStart, contextEnd, isRtl, end), 1.0f);
227
228                final float[] widths = new float[count];
229                p.getTextWidths(str, start, end, widths);
230                for (int i = 0; i < count; i++) {
231                    assertEquals(advanceArrays[0][i], widths[i], 1.0f);
232                }
233            }
234        }
235    }
236
237    public void testGetTextRunAdvances_invalid() {
238        Paint p = new Paint();
239        String text = "test";
240
241        try {
242            p.getTextRunAdvances((String)null, 0, 0, 0, 0, false, null, 0);
243            fail("Should throw an IllegalArgumentException.");
244        } catch (IllegalArgumentException e) {
245        }
246
247        try {
248            p.getTextRunAdvances((CharSequence)null, 0, 0, 0, 0, false, null, 0);
249            fail("Should throw an IllegalArgumentException.");
250        } catch (IllegalArgumentException e) {
251        }
252
253        try {
254            p.getTextRunAdvances((char[])null, 0, 0, 0, 0, false, null, 0);
255            fail("Should throw an IllegalArgumentException.");
256        } catch (IllegalArgumentException e) {
257        }
258
259        try {
260            p.getTextRunAdvances(text, 0, text.length(), 0, text.length(), false,
261                    new float[text.length() - 1], 0);
262            fail("Should throw an IndexOutOfBoundsException.");
263        } catch (IndexOutOfBoundsException e) {
264        }
265
266        try {
267            p.getTextRunAdvances(text, 0, text.length(), 0, text.length(), false,
268                    new float[text.length()], 1);
269            fail("Should throw an IndexOutOfBoundsException.");
270        } catch (IndexOutOfBoundsException e) {
271        }
272
273        // 0 > contextStart
274        try {
275            p.getTextRunAdvances(text, 0, text.length(), -1, text.length(), false, null, 0);
276            fail("Should throw an IndexOutOfBoundsException.");
277        } catch (IndexOutOfBoundsException e) {
278        }
279
280        // contextStart > start
281        try {
282            p.getTextRunAdvances(text, 0, text.length(), 1, text.length(), false, null, 0);
283            fail("Should throw an IndexOutOfBoundsException.");
284        } catch (IndexOutOfBoundsException e) {
285        }
286
287        // start > end
288        try {
289            p.getTextRunAdvances(text, 1, 0, 0, text.length(), false, null, 0);
290            fail("Should throw an IndexOutOfBoundsException.");
291        } catch (IndexOutOfBoundsException e) {
292        }
293
294        // end > contextEnd
295        try {
296            p.getTextRunAdvances(text, 0, text.length(), 0, text.length() - 1, false, null, 0);
297            fail("Should throw an IndexOutOfBoundsException.");
298        } catch (IndexOutOfBoundsException e) {
299        }
300
301        // contextEnd > text.length
302        try {
303            p.getTextRunAdvances(text, 0, text.length(), 0, text.length() + 1, false, null, 0);
304            fail("Should throw an IndexOutOfBoundsException.");
305        } catch (IndexOutOfBoundsException e) {
306        }
307    }
308
309    public void testMeasureTextBidi() {
310        Paint p = new Paint();
311        {
312            String bidiText = "abc \u0644\u063A\u0629 def";
313            p.setBidiFlags(Paint.BIDI_LTR);
314            float width = p.measureText(bidiText, 0, 4);
315            p.setBidiFlags(Paint.BIDI_RTL);
316            width += p.measureText(bidiText, 4, 7);
317            p.setBidiFlags(Paint.BIDI_LTR);
318            width += p.measureText(bidiText, 7, bidiText.length());
319            assertEquals(width, p.measureText(bidiText), 1.0f);
320        }
321        {
322            String bidiText = "abc \u0644\u063A\u0629 def";
323            p.setBidiFlags(Paint.BIDI_DEFAULT_LTR);
324            float width = p.measureText(bidiText, 0, 4);
325            width += p.measureText(bidiText, 4, 7);
326            width += p.measureText(bidiText, 7, bidiText.length());
327            assertEquals(width, p.measureText(bidiText), 1.0f);
328        }
329        {
330            String bidiText = "abc \u0644\u063A\u0629 def";
331            p.setBidiFlags(Paint.BIDI_FORCE_LTR);
332            float width = p.measureText(bidiText, 0, 4);
333            width += p.measureText(bidiText, 4, 7);
334            width += p.measureText(bidiText, 7, bidiText.length());
335            assertEquals(width, p.measureText(bidiText), 1.0f);
336        }
337        {
338            String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
339            p.setBidiFlags(Paint.BIDI_RTL);
340            float width = p.measureText(bidiText, 0, 4);
341            p.setBidiFlags(Paint.BIDI_LTR);
342            width += p.measureText(bidiText, 4, 7);
343            p.setBidiFlags(Paint.BIDI_RTL);
344            width += p.measureText(bidiText, 7, bidiText.length());
345            assertEquals(width, p.measureText(bidiText), 1.0f);
346        }
347        {
348            String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
349            p.setBidiFlags(Paint.BIDI_DEFAULT_RTL);
350            float width = p.measureText(bidiText, 0, 4);
351            width += p.measureText(bidiText, 4, 7);
352            width += p.measureText(bidiText, 7, bidiText.length());
353            assertEquals(width, p.measureText(bidiText), 1.0f);
354        }
355        {
356            String bidiText = "\u0644\u063A\u0629 abc \u0644\u063A\u0629";
357            p.setBidiFlags(Paint.BIDI_FORCE_RTL);
358            float width = p.measureText(bidiText, 0, 4);
359            width += p.measureText(bidiText, 4, 7);
360            width += p.measureText(bidiText, 7, bidiText.length());
361            assertEquals(width, p.measureText(bidiText), 1.0f);
362        }
363    }
364
365    public void testSetGetWordSpacing() {
366        Paint p = new Paint();
367        assertEquals(0.0f, p.getWordSpacing());  // The default value should be 0.
368        p.setWordSpacing(1.0f);
369        assertEquals(1.0f, p.getWordSpacing());
370        p.setWordSpacing(-2.0f);
371        assertEquals(-2.0f, p.getWordSpacing());
372    }
373
374    public void testGetUnderlinePositionAndThickness() {
375        final Typeface fontTypeface = Typeface.createFromAsset(
376                getInstrumentation().getContext().getAssets(), "fonts/underlineTestFont.ttf");
377        final Paint p = new Paint();
378        final int textSize = 100;
379        p.setTextSize(textSize);
380
381        final float origPosition = p.getUnderlinePosition();
382        final float origThickness = p.getUnderlineThickness();
383
384        p.setTypeface(fontTypeface);
385        assertNotEquals(origPosition, p.getUnderlinePosition());
386        assertNotEquals(origThickness, p.getUnderlineThickness());
387
388        //    -200 (underlinePosition in 'post' table, negative means below the baseline)
389        //    ÷ 1000 (unitsPerEm in 'head' table)
390        //    × 100 (text size)
391        //    × -1 (negated, since we consider below the baseline positive)
392        //    = 20
393        assertEquals(20.0f, p.getUnderlinePosition(), 0.5f);
394        //    300 (underlineThickness in 'post' table)
395        //    ÷ 1000 (unitsPerEm in 'head' table)
396        //    × 100 (text size)
397        //    = 30
398        assertEquals(30.0f, p.getUnderlineThickness(), 0.5f);
399    }
400}
401