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