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