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