• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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