1 /* 2 * Copyright (C) 2008 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 * These tests are taken from 17 * https://cs.android.com/android/platform/superproject/main/+/android12-dev:cts/tests/tests/graphics/src/android/graphics/text/cts/TextRunShaperTest.java 18 */ 19 20 package org.robolectric.shadows; 21 22 import static com.google.common.truth.Truth.assertThat; 23 import static org.junit.Assert.assertThrows; 24 25 import android.content.Context; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Paint; 30 import android.graphics.Typeface; 31 import android.graphics.fonts.Font; 32 import android.graphics.fonts.FontFamily; 33 import android.graphics.fonts.FontVariationAxis; 34 import android.graphics.text.PositionedGlyphs; 35 import android.graphics.text.TextRunShaper; 36 import android.text.Layout; 37 import android.text.TextDirectionHeuristic; 38 import android.text.TextDirectionHeuristics; 39 import android.text.TextPaint; 40 import androidx.test.core.app.ApplicationProvider; 41 import java.io.IOException; 42 import java.util.HashSet; 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 import org.robolectric.RobolectricTestRunner; 47 import org.robolectric.annotation.Config; 48 import org.robolectric.versioning.AndroidVersions.S; 49 50 @Config(minSdk = S.SDK_INT) 51 @RunWith(RobolectricTestRunner.class) 52 public class ShadowNativeTextRunShaperTest { 53 54 /** 55 * Perform static initialization on {@link PositionedGlyphs} to ensure that it can lazy-load RNG. 56 */ 57 @Before clinitPositionedGlyphs()58 public void clinitPositionedGlyphs() throws Exception { 59 Class.forName("android.graphics.text.PositionedGlyphs"); 60 } 61 62 @Test shapeText()63 public void shapeText() { 64 // Setup 65 Paint paint = new Paint(); 66 paint.setTextSize(100f); 67 String text = "Hello, World."; 68 69 // Act 70 PositionedGlyphs result = 71 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 72 73 // Assert 74 // Glyph must be included. (the count cannot be expected since there could be ligature). 75 assertThat(result.glyphCount()).isNotEqualTo(0); 76 for (int i = 0; i < result.glyphCount(); ++i) { 77 // Glyph ID = 0 is reserved for Tofu, thus expecting all character has glyph. 78 assertThat(result.getGlyphId(i)).isNotEqualTo(0); 79 } 80 81 // Must have horizontal advance. 82 assertThat(result.getAdvance()).isGreaterThan(0f); 83 float ascent = result.getAscent(); 84 float descent = result.getDescent(); 85 // Usually font has negative ascent value which is relative from the baseline. 86 assertThat(ascent).isLessThan(0f); 87 // Usually font has positive descent value which is relative from the baseline. 88 assertThat(descent).isGreaterThan(0f); 89 Paint.FontMetrics metrics = new Paint.FontMetrics(); 90 for (int i = 0; i < result.glyphCount(); ++i) { 91 result.getFont(i).getMetrics(paint, metrics); 92 // The overall ascent must be smaller (wider) than each font ascent. 93 assertThat(ascent <= metrics.ascent).isTrue(); 94 // The overall descent must be bigger (wider) than each font descent. 95 assertThat(descent >= metrics.descent).isTrue(); 96 } 97 } 98 99 @Test shapeText_context()100 public void shapeText_context() { 101 // Setup 102 Paint paint = new Paint(); 103 paint.setTextSize(100f); 104 105 // Arabic script change form (glyph) based on position. 106 String text = "\u0645\u0631\u062D\u0628\u0627"; 107 108 // Act 109 PositionedGlyphs resultWithContext = 110 TextRunShaper.shapeTextRun(text, 0, 1, 0, text.length(), 0f, 0f, true, paint); 111 PositionedGlyphs resultWithoutContext = 112 TextRunShaper.shapeTextRun(text, 0, 1, 0, 1, 0f, 0f, true, paint); 113 114 // Assert 115 assertThat(resultWithContext.getGlyphId(0)).isNotEqualTo(resultWithoutContext.getGlyphId(0)); 116 } 117 118 @Test shapeText_twoAPISameResult()119 public void shapeText_twoAPISameResult() { 120 // Setup 121 Paint paint = new Paint(); 122 String text = "Hello, World."; 123 paint.setTextSize(100f); // Shape text with 100px 124 125 // Act 126 PositionedGlyphs resultString = 127 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 128 129 char[] charArray = text.toCharArray(); 130 PositionedGlyphs resultChars = 131 TextRunShaper.shapeTextRun( 132 charArray, 0, charArray.length, 0, charArray.length, 0f, 0f, false, paint); 133 134 // Asserts 135 assertThat(resultString.glyphCount()).isEqualTo(resultChars.glyphCount()); 136 assertThat(resultString.getAdvance()).isEqualTo(resultChars.getAdvance()); 137 assertThat(resultString.getAscent()).isEqualTo(resultChars.getAscent()); 138 assertThat(resultString.getDescent()).isEqualTo(resultChars.getDescent()); 139 for (int i = 0; i < resultString.glyphCount(); ++i) { 140 assertThat(resultString.getGlyphId(i)).isEqualTo(resultChars.getGlyphId(i)); 141 assertThat(resultString.getFont(i)).isEqualTo(resultChars.getFont(i)); 142 assertThat(resultString.getGlyphX(i)).isEqualTo(resultChars.getGlyphX(i)); 143 assertThat(resultString.getGlyphY(i)).isEqualTo(resultChars.getGlyphY(i)); 144 } 145 } 146 147 @Test shapeText_multiLanguage()148 public void shapeText_multiLanguage() { 149 // Setup 150 Paint paint = new Paint(); 151 paint.setTextSize(100f); 152 String text = "Hello, Emoji: \uD83E\uDE90"; // Usually emoji is came from ColorEmoji font. 153 154 // Act 155 PositionedGlyphs result = 156 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 157 158 // Assert 159 HashSet<Font> set = new HashSet<>(); 160 for (int i = 0; i < result.glyphCount(); ++i) { 161 set.add(result.getFont(i)); 162 } 163 assertThat(set.size()).isEqualTo(2); // Roboto + Emoji is expected 164 } 165 166 @Test shapeText_fontCreateFromNative()167 public void shapeText_fontCreateFromNative() throws IOException { 168 // Setup 169 Context ctx = ApplicationProvider.getApplicationContext(); 170 Paint paint = new Paint(); 171 Font originalFont = 172 new Font.Builder(ctx.getAssets(), "fonts/WeightEqualsEmVariableFont.ttf").build(); 173 Typeface typeface = 174 new Typeface.CustomFallbackBuilder(new FontFamily.Builder(originalFont).build()).build(); 175 paint.setTypeface(typeface); 176 // setFontVariationSettings creates Typeface internally and it is not from Java Font object. 177 paint.setFontVariationSettings("'wght' 250"); 178 179 // Act 180 PositionedGlyphs res = TextRunShaper.shapeTextRun("a", 0, 1, 0, 1, 0f, 0f, false, paint); 181 182 // Assert 183 Font font = res.getFont(0); 184 assertThat(font.getBuffer()).isEqualTo(originalFont.getBuffer()); 185 assertThat(font.getTtcIndex()).isEqualTo(originalFont.getTtcIndex()); 186 FontVariationAxis[] axes = font.getAxes(); 187 assertThat(axes.length).isEqualTo(1); 188 assertThat(axes[0].getTag()).isEqualTo("wght"); 189 assertThat(axes[0].getStyleValue()).isEqualTo(250f); 190 } 191 192 @Test positionedGlyphs_equality()193 public void positionedGlyphs_equality() { 194 // Setup 195 Paint paint = new Paint(); 196 paint.setTextSize(100f); 197 198 // Act 199 PositionedGlyphs glyphs = TextRunShaper.shapeTextRun("abcde", 0, 5, 0, 5, 0f, 0f, true, paint); 200 PositionedGlyphs eqGlyphs = 201 TextRunShaper.shapeTextRun("abcde", 0, 5, 0, 5, 0f, 0f, true, paint); 202 PositionedGlyphs reversedGlyphs = 203 TextRunShaper.shapeTextRun("edcba", 0, 5, 0, 5, 0f, 0f, true, paint); 204 PositionedGlyphs substrGlyphs = 205 TextRunShaper.shapeTextRun("edcba", 0, 3, 0, 3, 0f, 0f, true, paint); 206 paint.setTextSize(50f); 207 PositionedGlyphs differentStyleGlyphs = 208 TextRunShaper.shapeTextRun("edcba", 0, 3, 0, 3, 0f, 0f, true, paint); 209 210 // Assert 211 assertThat(glyphs).isEqualTo(eqGlyphs); 212 213 assertThat(glyphs).isNotEqualTo(reversedGlyphs); 214 assertThat(glyphs).isNotEqualTo(substrGlyphs); 215 assertThat(glyphs).isNotEqualTo(differentStyleGlyphs); 216 } 217 218 @Test positionedGlyphs_illegalArgument_glyphID()219 public void positionedGlyphs_illegalArgument_glyphID() { 220 // Setup 221 Paint paint = new Paint(); 222 String text = "Hello, World."; 223 paint.setTextSize(100f); // Shape text with 100px 224 PositionedGlyphs res = 225 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 226 227 // Act 228 assertThrows( 229 IllegalArgumentException.class, 230 () -> res.getGlyphId(res.glyphCount())); // throws IllegalArgumentException 231 } 232 233 @Test resultTest_illegalArgument_font()234 public void resultTest_illegalArgument_font() { 235 // Setup 236 Paint paint = new Paint(); 237 String text = "Hello, World."; 238 paint.setTextSize(100f); // Shape text with 100px 239 PositionedGlyphs res = 240 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 241 242 // Act 243 assertThrows( 244 IllegalArgumentException.class, 245 () -> res.getFont(res.glyphCount())); // throws IllegalArgumentException 246 } 247 248 @Test resultTest_illegalArgument_x()249 public void resultTest_illegalArgument_x() { 250 // Setup 251 Paint paint = new Paint(); 252 String text = "Hello, World."; 253 paint.setTextSize(100f); // Shape text with 100px 254 PositionedGlyphs res = 255 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 256 257 // Act 258 assertThrows( 259 IllegalArgumentException.class, 260 () -> res.getGlyphX(res.glyphCount())); // throws IllegalArgumentException 261 } 262 263 @Test resultTest_illegalArgument_y()264 public void resultTest_illegalArgument_y() { 265 // Setup 266 Paint paint = new Paint(); 267 String text = "Hello, World."; 268 paint.setTextSize(100f); // Shape text with 100px 269 PositionedGlyphs res = 270 TextRunShaper.shapeTextRun(text, 0, text.length(), 0, text.length(), 0f, 0f, false, paint); 271 272 // Act 273 assertThrows( 274 IllegalArgumentException.class, 275 () -> res.getGlyphY(res.glyphCount())); // throws IllegalArgumentException 276 } 277 assertSameDrawResult( CharSequence text, TextPaint paint, TextDirectionHeuristic textDir)278 public void assertSameDrawResult( 279 CharSequence text, TextPaint paint, TextDirectionHeuristic textDir) { 280 int width = (int) Math.ceil(Layout.getDesiredWidth(text, paint)); 281 Paint.FontMetricsInt fmi = paint.getFontMetricsInt(); 282 int height = fmi.descent - fmi.ascent; 283 boolean isRtl = textDir.isRtl(text, 0, text.length()); 284 285 // Expected bitmap output 286 Bitmap layoutResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 287 Canvas layoutCanvas = new Canvas(layoutResult); 288 layoutCanvas.translate(0f, -fmi.ascent); 289 layoutCanvas.drawTextRun( 290 text, 291 0, 292 text.length(), // range 293 0, 294 text.length(), // context range 295 0f, 296 0f, // position 297 isRtl, 298 paint); 299 300 // Actual bitmap output 301 Bitmap glyphsResult = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 302 Canvas glyphsCanvas = new Canvas(glyphsResult); 303 glyphsCanvas.translate(0f, -fmi.ascent); 304 PositionedGlyphs glyphs = 305 TextRunShaper.shapeTextRun( 306 text, 307 0, 308 text.length(), // range 309 0, 310 text.length(), // context range 311 0f, 312 0f, // position 313 isRtl, 314 paint); 315 for (int i = 0; i < glyphs.glyphCount(); ++i) { 316 glyphsCanvas.drawGlyphs( 317 new int[] {glyphs.getGlyphId(i)}, 318 0, 319 new float[] {glyphs.getGlyphX(i), glyphs.getGlyphY(i)}, 320 0, 321 1, 322 glyphs.getFont(i), 323 paint); 324 } 325 326 assertThat(glyphsResult.sameAs(layoutResult)).isTrue(); 327 } 328 329 @Test testDrawConsistency()330 public void testDrawConsistency() { 331 TextPaint paint = new TextPaint(); 332 paint.setTextSize(32f); 333 paint.setColor(Color.BLUE); 334 assertSameDrawResult("Hello, Android.", paint, TextDirectionHeuristics.LTR); 335 } 336 337 @Test testDrawConsistencyMultiFont()338 public void testDrawConsistencyMultiFont() { 339 TextPaint paint = new TextPaint(); 340 paint.setTextSize(32f); 341 paint.setColor(Color.BLUE); 342 assertSameDrawResult("こんにちは、Android.", paint, TextDirectionHeuristics.LTR); 343 } 344 345 @Test testDrawConsistencyBidi()346 public void testDrawConsistencyBidi() { 347 TextPaint paint = new TextPaint(); 348 paint.setTextSize(32f); 349 paint.setColor(Color.BLUE); 350 assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); 351 assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.LTR); 352 assertSameDrawResult("مرحبا, Android.", paint, TextDirectionHeuristics.RTL); 353 } 354 355 @Test testDrawConsistencyBidi2()356 public void testDrawConsistencyBidi2() { 357 TextPaint paint = new TextPaint(); 358 paint.setTextSize(32f); 359 paint.setColor(Color.BLUE); 360 assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.FIRSTSTRONG_LTR); 361 assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.LTR); 362 assertSameDrawResult("Hello, العالمية", paint, TextDirectionHeuristics.RTL); 363 } 364 } 365