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