BidiRenderer.java revision 5ad7c183f39df43562c69aba21ea422ad69bdae0
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 java.awt.Font; 20import java.awt.Graphics2D; 21import java.awt.font.FontRenderContext; 22import java.awt.font.GlyphVector; 23import java.awt.geom.Rectangle2D; 24import java.util.LinkedList; 25import java.util.List; 26 27import com.ibm.icu.lang.UScript; 28import com.ibm.icu.lang.UScriptRun; 29 30import android.graphics.Paint_Delegate.FontInfo; 31import android.graphics.RectF;; 32 33/** 34 * Render the text by breaking it into various scripts and using the right font for each script. 35 * Can be used to measure the text without actually drawing it. 36 */ 37@SuppressWarnings("deprecation") 38public class BidiRenderer { 39 40 /* package */ static class ScriptRun { 41 int start; 42 int limit; 43 boolean isRtl; 44 int scriptCode; 45 FontInfo font; 46 47 public ScriptRun(int start, int limit, boolean isRtl) { 48 this.start = start; 49 this.limit = limit; 50 this.isRtl = isRtl; 51 this.scriptCode = UScript.INVALID_CODE; 52 } 53 } 54 55 private Graphics2D graphics; 56 private Paint_Delegate paint; 57 private char[] text; 58 // Bounds of the text drawn so far. 59 private RectF bounds; 60 61 /** 62 * @param graphics May be null. 63 * @param paint The Paint to use to get the fonts. Should not be null. 64 * @param text Unidirectional text. Should not be null. 65 */ 66 /* package */ BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) { 67 assert (paint != null); 68 this.graphics = graphics; 69 this.paint = paint; 70 this.text = text; 71 } 72 73 /** 74 * Render unidirectional text. 75 * 76 * This method can also be used to measure the width of the text without actually drawing it. 77 * 78 * @param start index of the first character 79 * @param limit index of the first character that should not be rendered. 80 * @param isRtl is the text right-to-left 81 * @param advances If not null, then advances for each character to be rendered are returned 82 * here. 83 * @param advancesIndex index into advances from where the advances need to be filled. 84 * @param draw If true and {@link graphics} is not null, draw the rendered text on the graphics 85 * at the given co-ordinates 86 * @param x The x-coordinate of the left edge of where the text should be drawn on the given 87 * graphics. 88 * @param y The y-coordinate at which to draw the text on the given graphics. 89 * @return A rectangle specifying the bounds of the text drawn. 90 */ 91 /* package */ RectF renderText(int start, int limit, boolean isRtl, float advances[], 92 int advancesIndex, boolean draw, float x, float y) { 93 // We break the text into scripts and then select font based on it and then render each of 94 // the script runs. 95 bounds = new RectF(x, y, x, y); 96 for (ScriptRun run : getScriptRuns(text, start, limit, isRtl, paint.getFonts())) { 97 int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT; 98 flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT; 99 renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw); 100 advancesIndex += run.limit - run.start; 101 } 102 return bounds; 103 } 104 105 /** 106 * Render a script run to the right of the bounds passed. Use the preferred font to render as 107 * much as possible. This also implements a fallback mechanism to render characters that cannot 108 * be drawn using the preferred font. 109 */ 110 private void renderScript(int start, int limit, FontInfo preferredFont, int flag, 111 float advances[], int advancesIndex, boolean draw) { 112 List<FontInfo> fonts = paint.getFonts(); 113 if (fonts == null || preferredFont == null) { 114 return; 115 } 116 117 while (start < limit) { 118 boolean foundFont = false; 119 int canDisplayUpTo = preferredFont.mFont.canDisplayUpTo(text, start, limit); 120 if (canDisplayUpTo == -1) { 121 // We can draw all characters in the text. 122 render(start, limit, preferredFont, flag, advances, advancesIndex, draw); 123 return; 124 } else if (canDisplayUpTo > start) { // can draw something 125 render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex, draw); 126 advancesIndex += canDisplayUpTo - start; 127 start = canDisplayUpTo; 128 } 129 130 // The current character cannot be drawn with the preferred font. Cycle through all the 131 // fonts to check which one can draw it. 132 int charCount = Character.isHighSurrogate(text[start]) ? 2 : 1; 133 for (FontInfo font : fonts) { 134 canDisplayUpTo = font.mFont.canDisplayUpTo(text, start, start + charCount); 135 if (canDisplayUpTo == -1) { 136 render(start, start+charCount, font, flag, advances, advancesIndex, draw); 137 start += charCount; 138 advancesIndex += charCount; 139 foundFont = true; 140 break; 141 } 142 } 143 if (!foundFont) { 144 // No font can display this char. Use the preferred font. The char will most 145 // probably appear as a box or a blank space. We could, probably, use some 146 // heuristics and break the character into the base character and diacritics and 147 // then draw it, but it's probably not worth the effort. 148 render(start, start + charCount, preferredFont, flag, advances, advancesIndex, 149 draw); 150 start += charCount; 151 advancesIndex += charCount; 152 } 153 } 154 } 155 156 /** 157 * Render the text with the given font to the right of the bounds passed. 158 */ 159 private void render(int start, int limit, FontInfo font, int flag, float advances[], 160 int advancesIndex, boolean draw) { 161 162 // Since the metrics don't have anti-aliasing set, we create a new FontRenderContext with 163 // the anti-aliasing set. 164 FontRenderContext f = font.mMetrics.getFontRenderContext(); 165 FontRenderContext frc = new FontRenderContext(f.getTransform(), paint.isAntiAliased(), 166 f.usesFractionalMetrics()); 167 GlyphVector gv = font.mFont.layoutGlyphVector(frc, text, start, limit, flag); 168 int ng = gv.getNumGlyphs(); 169 int[] ci = gv.getGlyphCharIndices(0, ng, null); 170 for (int i = 0; i < ng; i++) { 171 float adv = gv.getGlyphMetrics(i).getAdvanceX(); 172 if (advances != null) { 173 int adv_idx = advancesIndex + ci[i]; 174 advances[adv_idx] += adv; 175 } 176 } 177 if (draw && graphics != null) { 178 graphics.drawGlyphVector(gv, bounds.right, bounds.bottom); 179 } 180 Rectangle2D awtBounds = gv.getVisualBounds(); 181 RectF visualBounds = awtRectToAndroidRect(awtBounds, bounds.right, bounds.bottom); 182 // If the width of the bounds is zero, no text has been drawn yet. Hence, use the 183 // coordinates from the bounds as an offset only. 184 if (Math.abs(bounds.right - bounds.left) == 0) { 185 bounds = visualBounds; 186 } else { 187 bounds.union(visualBounds); 188 } 189 } 190 191 private RectF awtRectToAndroidRect(Rectangle2D awtRec, float offsetX, float offsetY) { 192 float left = (float) awtRec.getX(); 193 float top = (float) awtRec.getY(); 194 float right = (float) (left + awtRec.getWidth()); 195 float bottom = (float) (top + awtRec.getHeight()); 196 RectF androidRect = new RectF(left, top, right, bottom); 197 androidRect.offset(offsetX, offsetY); 198 return androidRect; 199 } 200 201 // --- Static helper methods --- 202 203 /* package */ static List<ScriptRun> getScriptRuns(char[] text, int start, int limit, 204 boolean isRtl, List<FontInfo> fonts) { 205 LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>(); 206 207 int count = limit - start; 208 UScriptRun uScriptRun = new UScriptRun(text, start, count); 209 while (uScriptRun.next()) { 210 int scriptStart = uScriptRun.getScriptStart(); 211 int scriptLimit = uScriptRun.getScriptLimit(); 212 ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl); 213 run.scriptCode = uScriptRun.getScriptCode(); 214 setScriptFont(text, run, fonts); 215 scriptRuns.add(run); 216 } 217 218 return scriptRuns; 219 } 220 221 // TODO: Replace this method with one which returns the font based on the scriptCode. 222 private static void setScriptFont(char[] text, ScriptRun run, 223 List<FontInfo> fonts) { 224 for (FontInfo fontInfo : fonts) { 225 if (fontInfo.mFont.canDisplayUpTo(text, run.start, run.limit) == -1) { 226 run.font = fontInfo; 227 return; 228 } 229 } 230 run.font = fonts.get(0); 231 } 232} 233