• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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 com.android.ide.common.rendering.api.LayoutLog;
20 import com.android.layoutlib.bridge.Bridge;
21 
22 import android.graphics.Paint_Delegate.FontInfo;
23 import android.icu.lang.UScript;
24 import android.icu.lang.UScriptRun;
25 import android.icu.text.Bidi;
26 import android.icu.text.BidiRun;
27 
28 import java.awt.Font;
29 import java.awt.Graphics2D;
30 import java.awt.Toolkit;
31 import java.awt.font.FontRenderContext;
32 import java.awt.font.GlyphVector;
33 import java.awt.geom.Rectangle2D;
34 import java.util.ArrayList;
35 import java.util.LinkedList;
36 import java.util.List;
37 
38 /**
39  * Render the text by breaking it into various scripts and using the right font for each script.
40  * Can be used to measure the text without actually drawing it.
41  */
42 @SuppressWarnings("deprecation")
43 public class BidiRenderer {
44 
45     private static class ScriptRun {
46         int start;
47         int limit;
48         boolean isRtl;
49         int scriptCode;
50         Font font;
51 
ScriptRun(int start, int limit, boolean isRtl)52         public ScriptRun(int start, int limit, boolean isRtl) {
53             this.start = start;
54             this.limit = limit;
55             this.isRtl = isRtl;
56             this.scriptCode = UScript.INVALID_CODE;
57         }
58     }
59 
60     private final Graphics2D mGraphics;
61     private final Paint_Delegate mPaint;
62     private char[] mText;
63     // This List can contain nulls. A null font implies that the we weren't able to load the font
64     // properly. So, if we encounter a situation where we try to use that font, log a warning.
65     private List<Font> mFonts;
66     // Bounds of the text drawn so far.
67     private RectF mBounds;
68     private float mBaseline;
69 
70     /**
71      * @param graphics May be null.
72      * @param paint The Paint to use to get the fonts. Should not be null.
73      * @param text Unidirectional text. Should not be null.
74      */
BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text)75     public BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) {
76         assert (paint != null);
77         mGraphics = graphics;
78         mPaint = paint;
79         mText = text;
80         mFonts = new ArrayList<Font>(paint.getFonts().size());
81         for (FontInfo fontInfo : paint.getFonts()) {
82             if (fontInfo == null) {
83                 mFonts.add(null);
84                 continue;
85             }
86             mFonts.add(fontInfo.mFont);
87         }
88         mBounds = new RectF();
89     }
90 
91     /**
92      *
93      * @param x The x-coordinate of the left edge of where the text should be drawn on the given
94      *            graphics.
95      * @param y The y-coordinate at which to draw the text on the given mGraphics.
96      *
97      */
setRenderLocation(float x, float y)98     public BidiRenderer setRenderLocation(float x, float y) {
99         mBounds = new RectF(x, y, x, y);
100         mBaseline = y;
101         return this;
102     }
103 
104     /**
105      * Perform Bidi Analysis on the text and then render it.
106      * <p/>
107      * To skip the analysis and render unidirectional text, see {@link
108      * #renderText(int, int, boolean, float[], int, boolean)}
109      */
renderText(int start, int limit, int bidiFlags, float[] advances, int advancesIndex, boolean draw)110     public RectF renderText(int start, int limit, int bidiFlags, float[] advances,
111             int advancesIndex, boolean draw) {
112         Bidi bidi = new Bidi(mText, start, null, 0, limit - start, getIcuFlags(bidiFlags));
113         for (int i = 0; i < bidi.countRuns(); i++) {
114             BidiRun visualRun = bidi.getVisualRun(i);
115             boolean isRtl = visualRun.getDirection() == Bidi.RTL;
116             renderText(visualRun.getStart(), visualRun.getLimit(), isRtl, advances,
117                     advancesIndex, draw);
118         }
119         return mBounds;
120     }
121 
122     /**
123      * Render unidirectional text.
124      * <p/>
125      * This method can also be used to measure the width of the text without actually drawing it.
126      * <p/>
127      * @param start index of the first character
128      * @param limit index of the first character that should not be rendered.
129      * @param isRtl is the text right-to-left
130      * @param advances If not null, then advances for each character to be rendered are returned
131      *            here.
132      * @param advancesIndex index into advances from where the advances need to be filled.
133      * @param draw If true and {@code graphics} is not null, draw the rendered text on the graphics
134      *            at the given co-ordinates
135      * @return A rectangle specifying the bounds of the text drawn.
136      */
renderText(int start, int limit, boolean isRtl, float[] advances, int advancesIndex, boolean draw)137     public RectF renderText(int start, int limit, boolean isRtl, float[] advances,
138             int advancesIndex, boolean draw) {
139         // We break the text into scripts and then select font based on it and then render each of
140         // the script runs.
141         for (ScriptRun run : getScriptRuns(mText, start, limit, isRtl, mFonts)) {
142             int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
143             flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
144             renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw);
145             advancesIndex += run.limit - run.start;
146         }
147         return mBounds;
148     }
149 
150     /**
151      * Render a script run to the right of the bounds passed. Use the preferred font to render as
152      * much as possible. This also implements a fallback mechanism to render characters that cannot
153      * be drawn using the preferred font.
154      */
renderScript(int start, int limit, Font preferredFont, int flag, float[] advances, int advancesIndex, boolean draw)155     private void renderScript(int start, int limit, Font preferredFont, int flag,
156             float[] advances, int advancesIndex, boolean draw) {
157         if (mFonts.size() == 0 || preferredFont == null) {
158             return;
159         }
160 
161         while (start < limit) {
162             boolean foundFont = false;
163             int canDisplayUpTo = preferredFont.canDisplayUpTo(mText, start, limit);
164             if (canDisplayUpTo == -1) {
165                 // We can draw all characters in the text.
166                 render(start, limit, preferredFont, flag, advances, advancesIndex, draw);
167                 return;
168             }
169             if (canDisplayUpTo > start) {
170                 // We can draw something.
171                 render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex, draw);
172                 advancesIndex += canDisplayUpTo - start;
173                 start = canDisplayUpTo;
174             }
175 
176             // The current character cannot be drawn with the preferred font. Cycle through all the
177             // fonts to check which one can draw it.
178             int charCount = Character.isHighSurrogate(mText[start]) ? 2 : 1;
179             for (Font font : mFonts) {
180                 if (font == null) {
181                     logFontWarning();
182                     continue;
183                 }
184                 canDisplayUpTo = font.canDisplayUpTo(mText, start, start + charCount);
185                 if (canDisplayUpTo == -1) {
186                     render(start, start+charCount, font, flag, advances, advancesIndex, draw);
187                     start += charCount;
188                     advancesIndex += charCount;
189                     foundFont = true;
190                     break;
191                 }
192             }
193             if (!foundFont) {
194                 // No font can display this char. Use the preferred font. The char will most
195                 // probably appear as a box or a blank space. We could, probably, use some
196                 // heuristics and break the character into the base character and diacritics and
197                 // then draw it, but it's probably not worth the effort.
198                 render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
199                         draw);
200                 start += charCount;
201                 advancesIndex += charCount;
202             }
203         }
204     }
205 
logFontWarning()206     private static void logFontWarning() {
207         Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
208                 "Some fonts could not be loaded. The rendering may not be perfect. " +
209                         "Try running the IDE with JRE 7.", null, null);
210     }
211 
212     /**
213      * Renders the text to the right of the bounds with the given font.
214      * @param font The font to render the text with.
215      */
render(int start, int limit, Font font, int flag, float[] advances, int advancesIndex, boolean draw)216     private void render(int start, int limit, Font font, int flag, float[] advances,
217             int advancesIndex, boolean draw) {
218 
219         FontRenderContext frc;
220         if (mGraphics != null) {
221             frc = mGraphics.getFontRenderContext();
222         } else {
223             frc = Toolkit.getDefaultToolkit().getFontMetrics(font).getFontRenderContext();
224             // Metrics obtained this way don't have anti-aliasing set. So,
225             // we create a new FontRenderContext with anti-aliasing set.
226             frc = new FontRenderContext(font.getTransform(), mPaint.isAntiAliased(), frc.usesFractionalMetrics());
227         }
228         GlyphVector gv = font.layoutGlyphVector(frc, mText, start, limit, flag);
229         int ng = gv.getNumGlyphs();
230         int[] ci = gv.getGlyphCharIndices(0, ng, null);
231         if (advances != null) {
232             for (int i = 0; i < ng; i++) {
233                 int adv_idx = advancesIndex + ci[i];
234                 advances[adv_idx] += gv.getGlyphMetrics(i).getAdvanceX();
235             }
236         }
237         if (draw && mGraphics != null) {
238             mGraphics.drawGlyphVector(gv, mBounds.right, mBaseline);
239         }
240 
241         // Update the bounds.
242         Rectangle2D awtBounds = gv.getLogicalBounds();
243         RectF bounds = awtRectToAndroidRect(awtBounds, mBounds.right, mBaseline);
244         // If the width of the bounds is zero, no text had been drawn earlier. Hence, use the
245         // coordinates from the bounds as an offset.
246         if (Math.abs(mBounds.right - mBounds.left) == 0) {
247             mBounds = bounds;
248         } else {
249             mBounds.union(bounds);
250         }
251     }
252 
253     // --- Static helper methods ---
254 
awtRectToAndroidRect(Rectangle2D awtRec, float offsetX, float offsetY)255     private static RectF awtRectToAndroidRect(Rectangle2D awtRec, float offsetX, float offsetY) {
256         float left = (float) awtRec.getX();
257         float top = (float) awtRec.getY();
258         float right = (float) (left + awtRec.getWidth());
259         float bottom = (float) (top + awtRec.getHeight());
260         RectF androidRect = new RectF(left, top, right, bottom);
261         androidRect.offset(offsetX, offsetY);
262         return androidRect;
263     }
264 
getScriptRuns(char[] text, int start, int limit, boolean isRtl, List<Font> fonts)265     /* package */  static List<ScriptRun> getScriptRuns(char[] text, int start, int limit,
266             boolean isRtl, List<Font> fonts) {
267         LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>();
268 
269         int count = limit - start;
270         UScriptRun uScriptRun = new UScriptRun(text, start, count);
271         while (uScriptRun.next()) {
272             int scriptStart = uScriptRun.getScriptStart();
273             int scriptLimit = uScriptRun.getScriptLimit();
274             ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl);
275             run.scriptCode = uScriptRun.getScriptCode();
276             setScriptFont(text, run, fonts);
277             scriptRuns.add(run);
278         }
279 
280         return scriptRuns;
281     }
282 
283     // TODO: Replace this method with one which returns the font based on the scriptCode.
setScriptFont(char[] text, ScriptRun run, List<Font> fonts)284     private static void setScriptFont(char[] text, ScriptRun run,
285             List<Font> fonts) {
286         for (Font font : fonts) {
287             if (font == null) {
288                 logFontWarning();
289                 continue;
290             }
291             if (font.canDisplayUpTo(text, run.start, run.limit) == -1) {
292                 run.font = font;
293                 return;
294             }
295         }
296         run.font = fonts.get(0);
297     }
298 
getIcuFlags(int bidiFlag)299     private static int getIcuFlags(int bidiFlag) {
300         switch (bidiFlag) {
301             case Paint.BIDI_LTR:
302             case Paint.BIDI_FORCE_LTR:
303                 return Bidi.DIRECTION_LEFT_TO_RIGHT;
304             case Paint.BIDI_RTL:
305             case Paint.BIDI_FORCE_RTL:
306                 return Bidi.DIRECTION_RIGHT_TO_LEFT;
307             case Paint.BIDI_DEFAULT_LTR:
308                 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
309             case Paint.BIDI_DEFAULT_RTL:
310                 return Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT;
311             default:
312                 assert false;
313                 return Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT;
314         }
315     }
316 }
317