Home | History | Annotate | Download | only in graphics
      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 java.awt.Font;
     20 import java.awt.Graphics2D;
     21 import java.awt.font.FontRenderContext;
     22 import java.awt.font.GlyphVector;
     23 import java.util.LinkedList;
     24 import java.util.List;
     25 
     26 import com.ibm.icu.lang.UScript;
     27 import com.ibm.icu.lang.UScriptRun;
     28 
     29 import android.graphics.Paint_Delegate.FontInfo;
     30 
     31 /**
     32  * Render the text by breaking it into various scripts and using the right font for each script.
     33  * Can be used to measure the text without actually drawing it.
     34  */
     35 @SuppressWarnings("deprecation")
     36 public class BidiRenderer {
     37 
     38     /* package */ static class ScriptRun {
     39         int start;
     40         int limit;
     41         boolean isRtl;
     42         int scriptCode;
     43         FontInfo font;
     44 
     45         public ScriptRun(int start, int limit, boolean isRtl) {
     46             this.start = start;
     47             this.limit = limit;
     48             this.isRtl = isRtl;
     49             this.scriptCode = UScript.INVALID_CODE;
     50         }
     51     }
     52 
     53     /* package */ Graphics2D graphics;
     54     /* package */ Paint_Delegate paint;
     55     /* package */ char[] text;
     56 
     57     /**
     58      * @param graphics May be null.
     59      * @param paint The Paint to use to get the fonts. Should not be null.
     60      * @param text Unidirectional text. Should not be null.
     61      */
     62     /* package */ BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) {
     63         assert (paint != null);
     64         this.graphics = graphics;
     65         this.paint = paint;
     66         this.text = text;
     67     }
     68 
     69     /**
     70      * Render unidirectional text.
     71      *
     72      * This method can also be used to measure the width of the text without actually drawing it.
     73      *
     74      * @param start index of the first character
     75      * @param limit index of the first character that should not be rendered.
     76      * @param isRtl is the text right-to-left
     77      * @param advances If not null, then advances for each character to be rendered are returned
     78      *            here.
     79      * @param advancesIndex index into advances from where the advances need to be filled.
     80      * @param draw If true and {@link graphics} is not null, draw the rendered text on the graphics
     81      *            at the given co-ordinates
     82      * @param x The x-coordinate of the left edge of where the text should be drawn on the given
     83      *            graphics.
     84      * @param y The y-coordinate at which to draw the text on the given graphics.
     85      * @return The x-coordinate of the right edge of the drawn text. In other words,
     86      *            x + the width of the text.
     87      */
     88     /* package */ float renderText(int start, int limit, boolean isRtl, float advances[],
     89             int advancesIndex, boolean draw, float x, float y) {
     90         // We break the text into scripts and then select font based on it and then render each of
     91         // the script runs.
     92         for (ScriptRun run : getScriptRuns(text, start, limit, isRtl, paint.getFonts())) {
     93             int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
     94             flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
     95             x = renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw,
     96                     x, y);
     97             advancesIndex += run.limit - run.start;
     98         }
     99         return x;
    100     }
    101 
    102     /**
    103      * Render a script run. Use the preferred font to render as much as possible. This also
    104      * implements a fallback mechanism to render characters that cannot be drawn using the
    105      * preferred font.
    106      *
    107      * @return x + width of the text drawn.
    108      */
    109     private float renderScript(int start, int limit, FontInfo preferredFont, int flag,
    110             float advances[], int advancesIndex, boolean draw, float x, float y) {
    111         List<FontInfo> fonts = paint.getFonts();
    112         if (fonts == null || preferredFont == null) {
    113             return x;
    114         }
    115 
    116         while (start < limit) {
    117             boolean foundFont = false;
    118             int canDisplayUpTo = preferredFont.mFont.canDisplayUpTo(text, start, limit);
    119             if (canDisplayUpTo == -1) {
    120                 return render(start, limit, preferredFont, flag, advances, advancesIndex, draw,
    121                         x, y);
    122             } else if (canDisplayUpTo > start) { // can draw something
    123                 x = render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex,
    124                         draw, x, y);
    125                 advancesIndex += canDisplayUpTo - start;
    126                 start = canDisplayUpTo;
    127             }
    128 
    129             int charCount = Character.isHighSurrogate(text[start]) ? 2 : 1;
    130             for (FontInfo font : fonts) {
    131                 canDisplayUpTo = font.mFont.canDisplayUpTo(text, start, start + charCount);
    132                 if (canDisplayUpTo == -1) {
    133                     x = render(start, start+charCount, font, flag, advances, advancesIndex, draw,
    134                             x, y);
    135                     start += charCount;
    136                     advancesIndex += charCount;
    137                     foundFont = true;
    138                     break;
    139                 }
    140             }
    141             if (!foundFont) {
    142                 // No font can display this char. Use the preferred font. The char will most
    143                 // probably appear as a box or a blank space. We could, probably, use some
    144                 // heuristics and break the character into the base character and diacritics and
    145                 // then draw it, but it's probably not worth the effort.
    146                 x = render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
    147                         draw, x, y);
    148                 start += charCount;
    149                 advancesIndex += charCount;
    150             }
    151         }
    152         return x;
    153     }
    154 
    155     /**
    156      * Render the text with the given font.
    157      */
    158     private float render(int start, int limit, FontInfo font, int flag, float advances[],
    159             int advancesIndex, boolean draw, float x, float y) {
    160 
    161         float totalAdvance = 0;
    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             totalAdvance += adv;
    177         }
    178         if (draw && graphics != null) {
    179             graphics.drawGlyphVector(gv, x, y);
    180         }
    181         return x + totalAdvance;
    182     }
    183 
    184     // --- Static helper methods ---
    185 
    186     /* package */  static List<ScriptRun> getScriptRuns(char[] text, int start, int limit,
    187             boolean isRtl, List<FontInfo> fonts) {
    188         LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>();
    189 
    190         int count = limit - start;
    191         UScriptRun uScriptRun = new UScriptRun(text, start, count);
    192         while (uScriptRun.next()) {
    193             int scriptStart = uScriptRun.getScriptStart();
    194             int scriptLimit = uScriptRun.getScriptLimit();
    195             ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl);
    196             run.scriptCode = uScriptRun.getScriptCode();
    197             setScriptFont(text, run, fonts);
    198             scriptRuns.add(run);
    199         }
    200 
    201         return scriptRuns;
    202     }
    203 
    204     // TODO: Replace this method with one which returns the font based on the scriptCode.
    205     private static void setScriptFont(char[] text, ScriptRun run,
    206             List<FontInfo> fonts) {
    207         for (FontInfo fontInfo : fonts) {
    208             if (fontInfo.mFont.canDisplayUpTo(text, run.start, run.limit) == -1) {
    209                 run.font = fontInfo;
    210                 return;
    211             }
    212         }
    213         run.font = fonts.get(0);
    214     }
    215 }
    216