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;
31
32/**
33 * Render the text by breaking it into various scripts and using the right font for each script.
34 * Can be used to measure the text without actually drawing it.
35 */
36@SuppressWarnings("deprecation")
37public class BidiRenderer {
38
39    /* package */ static class ScriptRun {
40        int start;
41        int limit;
42        boolean isRtl;
43        int scriptCode;
44        FontInfo font;
45
46        public ScriptRun(int start, int limit, boolean isRtl) {
47            this.start = start;
48            this.limit = limit;
49            this.isRtl = isRtl;
50            this.scriptCode = UScript.INVALID_CODE;
51        }
52    }
53
54    private Graphics2D mGraphics;
55    private Paint_Delegate mPaint;
56    private char[] mText;
57    // Bounds of the text drawn so far.
58    private RectF mBounds;
59    private float mBaseline;
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        mGraphics = graphics;
69        mPaint = paint;
70        mText = 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 {@code 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 mGraphics.
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        mBounds = new RectF(x, y, x, y);
96        mBaseline = y;
97        for (ScriptRun run : getScriptRuns(mText, start, limit, isRtl, mPaint.getFonts())) {
98            int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
99            flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
100            renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw);
101            advancesIndex += run.limit - run.start;
102        }
103        return mBounds;
104    }
105
106    /**
107     * Render a script run to the right of the bounds passed. Use the preferred font to render as
108     * much as possible. This also implements a fallback mechanism to render characters that cannot
109     * be drawn using the preferred font.
110     */
111    private void renderScript(int start, int limit, FontInfo preferredFont, int flag,
112            float[] advances, int advancesIndex, boolean draw) {
113        List<FontInfo> fonts = mPaint.getFonts();
114        if (fonts == null || preferredFont == null) {
115            return;
116        }
117
118        while (start < limit) {
119            boolean foundFont = false;
120            int canDisplayUpTo = preferredFont.mFont.canDisplayUpTo(mText, start, limit);
121            if (canDisplayUpTo == -1) {
122                // We can draw all characters in the text.
123                render(start, limit, preferredFont, flag, advances, advancesIndex, draw);
124                return;
125            }
126            if (canDisplayUpTo > start) {
127                // We can draw something.
128                render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex, draw);
129                advancesIndex += canDisplayUpTo - start;
130                start = canDisplayUpTo;
131            }
132
133            // The current character cannot be drawn with the preferred font. Cycle through all the
134            // fonts to check which one can draw it.
135            int charCount = Character.isHighSurrogate(mText[start]) ? 2 : 1;
136            for (FontInfo font : fonts) {
137                canDisplayUpTo = font.mFont.canDisplayUpTo(mText, start, start + charCount);
138                if (canDisplayUpTo == -1) {
139                    render(start, start+charCount, font, flag, advances, advancesIndex, draw);
140                    start += charCount;
141                    advancesIndex += charCount;
142                    foundFont = true;
143                    break;
144                }
145            }
146            if (!foundFont) {
147                // No font can display this char. Use the preferred font. The char will most
148                // probably appear as a box or a blank space. We could, probably, use some
149                // heuristics and break the character into the base character and diacritics and
150                // then draw it, but it's probably not worth the effort.
151                render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
152                        draw);
153                start += charCount;
154                advancesIndex += charCount;
155            }
156        }
157    }
158
159    /**
160     * Renders the text to the right of the bounds with the given font.
161     * @param font The font to render the text with.
162     */
163    private void render(int start, int limit, FontInfo font, int flag, float[] advances,
164            int advancesIndex, boolean draw) {
165
166        // Since the metrics don't have anti-aliasing set, we create a new FontRenderContext with
167        // the anti-aliasing set.
168        FontRenderContext f = font.mMetrics.getFontRenderContext();
169        FontRenderContext frc = new FontRenderContext(f.getTransform(), mPaint.isAntiAliased(),
170                f.usesFractionalMetrics());
171        GlyphVector gv = font.mFont.layoutGlyphVector(frc, mText, start, limit, flag);
172        int ng = gv.getNumGlyphs();
173        int[] ci = gv.getGlyphCharIndices(0, ng, null);
174        if (advances != null) {
175            for (int i = 0; i < ng; i++) {
176                int adv_idx = advancesIndex + ci[i];
177                advances[adv_idx] += gv.getGlyphMetrics(i).getAdvanceX();
178            }
179        }
180        if (draw && mGraphics != null) {
181            mGraphics.drawGlyphVector(gv, mBounds.right, mBaseline);
182        }
183
184        // Update the bounds.
185        Rectangle2D awtBounds = gv.getLogicalBounds();
186        RectF bounds = awtRectToAndroidRect(awtBounds, mBounds.right, mBaseline);
187        // If the width of the bounds is zero, no text had been drawn earlier. Hence, use the
188        // coordinates from the bounds as an offset.
189        if (Math.abs(mBounds.right - mBounds.left) == 0) {
190            mBounds = bounds;
191        } else {
192            mBounds.union(bounds);
193        }
194    }
195
196    // --- Static helper methods ---
197
198    private static RectF awtRectToAndroidRect(Rectangle2D awtRec, float offsetX, float offsetY) {
199        float left = (float) awtRec.getX();
200        float top = (float) awtRec.getY();
201        float right = (float) (left + awtRec.getWidth());
202        float bottom = (float) (top + awtRec.getHeight());
203        RectF androidRect = new RectF(left, top, right, bottom);
204        androidRect.offset(offsetX, offsetY);
205        return androidRect;
206    }
207
208    /* package */  static List<ScriptRun> getScriptRuns(char[] text, int start, int limit,
209            boolean isRtl, List<FontInfo> fonts) {
210        LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>();
211
212        int count = limit - start;
213        UScriptRun uScriptRun = new UScriptRun(text, start, count);
214        while (uScriptRun.next()) {
215            int scriptStart = uScriptRun.getScriptStart();
216            int scriptLimit = uScriptRun.getScriptLimit();
217            ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl);
218            run.scriptCode = uScriptRun.getScriptCode();
219            setScriptFont(text, run, fonts);
220            scriptRuns.add(run);
221        }
222
223        return scriptRuns;
224    }
225
226    // TODO: Replace this method with one which returns the font based on the scriptCode.
227    private static void setScriptFont(char[] text, ScriptRun run,
228            List<FontInfo> fonts) {
229        for (FontInfo fontInfo : fonts) {
230            if (fontInfo.mFont.canDisplayUpTo(text, run.start, run.limit) == -1) {
231                run.font = fontInfo;
232                return;
233            }
234        }
235        run.font = fonts.get(0);
236    }
237}
238