Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3a762d8b authored by Deepanshu Gupta's avatar Deepanshu Gupta Committed by Android (Google) Code Review
Browse files

Merge "Fix text rendering" into jb-mr2-dev

parents 68411471 0d9c922c
Loading
Loading
Loading
Loading
+215 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics;

import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.util.LinkedList;
import java.util.List;

import com.ibm.icu.lang.UScript;
import com.ibm.icu.lang.UScriptRun;

import android.graphics.Paint_Delegate.FontInfo;

/**
 * Render the text by breaking it into various scripts and using the right font for each script.
 * Can be used to measure the text without actually drawing it.
 */
@SuppressWarnings("deprecation")
public class BidiRenderer {

    /* package */ static class ScriptRun {
        int start;
        int limit;
        boolean isRtl;
        int scriptCode;
        FontInfo font;

        public ScriptRun(int start, int limit, boolean isRtl) {
            this.start = start;
            this.limit = limit;
            this.isRtl = isRtl;
            this.scriptCode = UScript.INVALID_CODE;
        }
    }

    /* package */ Graphics2D graphics;
    /* package */ Paint_Delegate paint;
    /* package */ char[] text;

    /**
     * @param graphics May be null.
     * @param paint The Paint to use to get the fonts. Should not be null.
     * @param text Unidirectional text. Should not be null.
     */
    /* package */ BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) {
        assert (paint != null);
        this.graphics = graphics;
        this.paint = paint;
        this.text = text;
    }

    /**
     * Render unidirectional text.
     *
     * This method can also be used to measure the width of the text without actually drawing it.
     *
     * @param start index of the first character
     * @param limit index of the first character that should not be rendered.
     * @param isRtl is the text right-to-left
     * @param advances If not null, then advances for each character to be rendered are returned
     *            here.
     * @param advancesIndex index into advances from where the advances need to be filled.
     * @param draw If true and {@link graphics} is not null, draw the rendered text on the graphics
     *            at the given co-ordinates
     * @param x The x-coordinate of the left edge of where the text should be drawn on the given
     *            graphics.
     * @param y The y-coordinate at which to draw the text on the given graphics.
     * @return The x-coordinate of the right edge of the drawn text. In other words,
     *            x + the width of the text.
     */
    /* package */ float renderText(int start, int limit, boolean isRtl, float advances[],
            int advancesIndex, boolean draw, float x, float y) {
        // We break the text into scripts and then select font based on it and then render each of
        // the script runs.
        for (ScriptRun run : getScriptRuns(text, start, limit, isRtl, paint.getFonts())) {
            int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
            flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
            x = renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw,
                    x, y);
            advancesIndex += run.limit - run.start;
        }
        return x;
    }

    /**
     * Render a script run. Use the preferred font to render as much as possible. This also
     * implements a fallback mechanism to render characters that cannot be drawn using the
     * preferred font.
     *
     * @return x + width of the text drawn.
     */
    private float renderScript(int start, int limit, FontInfo preferredFont, int flag,
            float advances[], int advancesIndex, boolean draw, float x, float y) {
        List<FontInfo> fonts = paint.getFonts();
        if (fonts == null || preferredFont == null) {
            return x;
        }

        while (start < limit) {
            boolean foundFont = false;
            int canDisplayUpTo = preferredFont.mFont.canDisplayUpTo(text, start, limit);
            if (canDisplayUpTo == -1) {
                return render(start, limit, preferredFont, flag, advances, advancesIndex, draw,
                        x, y);
            } else if (canDisplayUpTo > start) { // can draw something
                x = render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex,
                        draw, x, y);
                advancesIndex += canDisplayUpTo - start;
                start = canDisplayUpTo;
            }

            int charCount = Character.isHighSurrogate(text[start]) ? 2 : 1;
            for (FontInfo font : fonts) {
                canDisplayUpTo = font.mFont.canDisplayUpTo(text, start, start + charCount);
                if (canDisplayUpTo == -1) {
                    x = render(start, start+charCount, font, flag, advances, advancesIndex, draw,
                            x, y);
                    start += charCount;
                    advancesIndex += charCount;
                    foundFont = true;
                    break;
                }
            }
            if (!foundFont) {
                // No font can display this char. Use the preferred font. The char will most
                // probably appear as a box or a blank space. We could, probably, use some
                // heuristics and break the character into the base character and diacritics and
                // then draw it, but it's probably not worth the effort.
                x = render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
                        draw, x, y);
                start += charCount;
                advancesIndex += charCount;
            }
        }
        return x;
    }

    /**
     * Render the text with the given font.
     */
    private float render(int start, int limit, FontInfo font, int flag, float advances[],
            int advancesIndex, boolean draw, float x, float y) {

        float totalAdvance = 0;
        // Since the metrics don't have anti-aliasing set, we create a new FontRenderContext with
        // the anti-aliasing set.
        FontRenderContext f = font.mMetrics.getFontRenderContext();
        FontRenderContext frc = new FontRenderContext(f.getTransform(), paint.isAntiAliased(),
                f.usesFractionalMetrics());
        GlyphVector gv = font.mFont.layoutGlyphVector(frc, text, start, limit, flag);
        int ng = gv.getNumGlyphs();
        int[] ci = gv.getGlyphCharIndices(0, ng, null);
        for (int i = 0; i < ng; i++) {
            float adv = gv.getGlyphMetrics(i).getAdvanceX();
            if (advances != null) {
                int adv_idx = advancesIndex + ci[i];
                advances[adv_idx] += adv;
            }
            totalAdvance += adv;
        }
        if (draw && graphics != null) {
            graphics.drawGlyphVector(gv, x, y);
        }
        return x + totalAdvance;
    }

    // --- Static helper methods ---

    /* package */  static List<ScriptRun> getScriptRuns(char[] text, int start, int limit,
            boolean isRtl, List<FontInfo> fonts) {
        LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>();

        int count = limit - start;
        UScriptRun uScriptRun = new UScriptRun(text, start, count);
        while (uScriptRun.next()) {
            int scriptStart = uScriptRun.getScriptStart();
            int scriptLimit = uScriptRun.getScriptLimit();
            ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl);
            run.scriptCode = uScriptRun.getScriptCode();
            setScriptFont(text, run, fonts);
            scriptRuns.add(run);
        }

        return scriptRuns;
    }

    // TODO: Replace this method with one which returns the font based on the scriptCode.
    private static void setScriptFont(char[] text, ScriptRun run,
            List<FontInfo> fonts) {
        for (FontInfo fontInfo : fonts) {
            if (fontInfo.mFont.canDisplayUpTo(text, run.start, run.limit) == -1) {
                run.font = fontInfo;
                return;
            }
        }
        run.font = fonts.get(0);
    }
}
+8 −81
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import com.android.layoutlib.bridge.impl.GcSnapshot;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;

import android.graphics.Bitmap.Config;
import android.graphics.Paint_Delegate.FontInfo;
import android.text.TextUtils;

import java.awt.Color;
@@ -35,7 +34,6 @@ import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.image.BufferedImage;
import java.util.List;


/**
@@ -978,7 +976,8 @@ public final class Canvas_Delegate {
    @LayoutlibDelegate
    /*package*/ static void native_drawText(int nativeCanvas,
            final char[] text, final int index, final int count,
            final float startX, final float startY, int flags, int paint) {
            final float startX, final float startY, final int flags, int paint) {

        draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
                new GcSnapshot.Drawable() {
            @Override
@@ -988,10 +987,10 @@ public final class Canvas_Delegate {
                // Paint.TextAlign indicates how the text is positioned relative to X.
                // LEFT is the default and there's nothing to do.
                float x = startX;
                float y = startY;
                int limit = index + count;
                boolean isRtl = flags == Canvas.DIRECTION_RTL;
                if (paintDelegate.getTextAlign() != Paint.Align.LEFT.nativeInt) {
                    // TODO: check the value of bidiFlags.
                    float m = paintDelegate.measureText(text, index, count, 0);
                    float m = paintDelegate.measureText(text, index, count, isRtl);
                    if (paintDelegate.getTextAlign() == Paint.Align.CENTER.nativeInt) {
                        x -= m / 2;
                    } else if (paintDelegate.getTextAlign() == Paint.Align.RIGHT.nativeInt) {
@@ -999,87 +998,15 @@ public final class Canvas_Delegate {
                    }
                }

                List<FontInfo> fonts = paintDelegate.getFonts();

                if (fonts.size() > 0) {
                    FontInfo mainFont = fonts.get(0);
                    int i = index;
                    int lastIndex = index + count;
                    while (i < lastIndex) {
                        // always start with the main font.
                        int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex);
                        if (upTo == -1) {
                            // draw all the rest and exit.
                            graphics.setFont(mainFont.mFont);
                            graphics.drawChars(text, i, lastIndex - i, (int)x, (int)y);
                            return;
                        } else if (upTo > 0) {
                            // draw what's possible
                            graphics.setFont(mainFont.mFont);
                            graphics.drawChars(text, i, upTo - i, (int)x, (int)y);

                            // compute the width that was drawn to increase x
                            x += mainFont.mMetrics.charsWidth(text, i, upTo - i);

                            // move index to the first non displayed char.
                            i = upTo;

                            // don't call continue at this point. Since it is certain the main font
                            // cannot display the font a index upTo (now ==i), we move on to the
                            // fallback fonts directly.
                        }

                        // no char supported, attempt to read the next char(s) with the
                        // fallback font. In this case we only test the first character
                        // and then go back to test with the main font.
                        // Special test for 2-char characters.
                        boolean foundFont = false;
                        for (int f = 1 ; f < fonts.size() ; f++) {
                            FontInfo fontInfo = fonts.get(f);

                            // need to check that the font can display the character. We test
                            // differently if the char is a high surrogate.
                            int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
                            upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount);
                            if (upTo == -1) {
                                // draw that char
                                graphics.setFont(fontInfo.mFont);
                                graphics.drawChars(text, i, charCount, (int)x, (int)y);

                                // update x
                                x += fontInfo.mMetrics.charsWidth(text, i, charCount);

                                // update the index in the text, and move on
                                i += charCount;
                                foundFont = true;
                                break;

                            }
                        }

                        // in case no font can display the char, display it with the main font.
                        // (it'll put a square probably)
                        if (foundFont == false) {
                            int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;

                            graphics.setFont(mainFont.mFont);
                            graphics.drawChars(text, i, charCount, (int)x, (int)y);

                            // measure it to advance x
                            x += mainFont.mMetrics.charsWidth(text, i, charCount);

                            // and move to the next chars.
                            i += charCount;
                        }
                    }
                }
                new BidiRenderer(graphics, paintDelegate, text).renderText(
                        index, limit, isRtl, null, 0, true, x, startY);
            }
        });
    }

    @LayoutlibDelegate
    /*package*/ static void native_drawText(int nativeCanvas, String text,
            int start, int end, float x, float y, int flags, int paint) {
            int start, int end, float x, float y, final int flags, int paint) {
        int count = end - start;
        char[] buffer = TemporaryBuffer.obtain(count);
        TextUtils.getChars(text, start, end, buffer, 0);
+30 −106
Original line number Diff line number Diff line
@@ -32,7 +32,6 @@ import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -576,7 +575,7 @@ public class Paint_Delegate {
            return 0;
        }

        return delegate.measureText(text, index, count, bidiFlags);
        return delegate.measureText(text, index, count, isRtl(bidiFlags));
    }

    @LayoutlibDelegate
@@ -615,7 +614,7 @@ public class Paint_Delegate {
            }

            // measure from start to end
            float res = delegate.measureText(text, start, end - start + 1, bidiFlags);
            float res = delegate.measureText(text, start, end - start + 1, isRtl(bidiFlags));

            if (measuredWidth != null) {
                measuredWidth[measureIndex] = res;
@@ -980,51 +979,27 @@ public class Paint_Delegate {
    /*package*/ static float native_getTextRunAdvances(int native_object,
            char[] text, int index, int count, int contextIndex, int contextCount,
            int flags, float[] advances, int advancesIndex) {

        if (advances != null)
            for (int i = advancesIndex; i< advancesIndex+count; i++)
                advances[i]=0;
        // get the delegate from the native int.
        Paint_Delegate delegate = sManager.getDelegate(native_object);
        if (delegate == null) {
        if (delegate == null || delegate.mFonts == null || delegate.mFonts.size() == 0) {
            return 0.f;
        }
        boolean isRtl = isRtl(flags);

        if (delegate.mFonts.size() > 0) {
            // FIXME: handle multi-char characters (see measureText)
            float totalAdvance = 0;
            for (int i = 0; i < count; i++) {
                char c = text[i + index];
                boolean found = false;
                for (FontInfo info : delegate.mFonts) {
                    if (info.mFont.canDisplay(c)) {
                        float adv = info.mMetrics.charWidth(c);
                        totalAdvance += adv;
                        if (advances != null) {
                            advances[i] = adv;
                        }

                        found = true;
                        break;
                    }
                }

                if (found == false) {
                    // no advance for this char.
                    if (advances != null) {
                        advances[i] = 0.f;
                    }
                }
            }

            return totalAdvance;
        }

        return 0;

        int limit = index + count;
        return new BidiRenderer(null, delegate, text).renderText(
                index, limit, isRtl, advances, advancesIndex, false, 0, 0);
    }

    @LayoutlibDelegate
    /*package*/ static float native_getTextRunAdvances(int native_object,
            String text, int start, int end, int contextStart, int contextEnd,
            int flags, float[] advances, int advancesIndex) {
        // FIXME: support contextStart, contextEnd and direction flag
        // FIXME: support contextStart and contextEnd
        int count = end - start;
        char[] buffer = TemporaryBuffer.obtain(count);
        TextUtils.getChars(text, start, end, buffer, 0);
@@ -1080,19 +1055,12 @@ public class Paint_Delegate {

        // get the delegate from the native int.
        Paint_Delegate delegate = sManager.getDelegate(nativePaint);
        if (delegate == null) {
        if (delegate == null || delegate.mFonts == null || delegate.mFonts.size() == 0) {
            return;
        }

        // FIXME should test if the main font can display all those characters.
        // See MeasureText
        if (delegate.mFonts.size() > 0) {
            FontInfo mainInfo = delegate.mFonts.get(0);

            Rectangle2D rect = mainInfo.mFont.getStringBounds(text, index, index + count,
                    delegate.mFontContext);
            bounds.set(0, 0, (int) rect.getWidth(), (int) rect.getHeight());
        }
        int w = (int) delegate.measureText(text, index, count, isRtl(bidiFlags));
        int h= delegate.getFonts().get(0).mMetrics.getHeight();
        bounds.set(0, 0, w, h);
    }

    @LayoutlibDelegate
@@ -1176,6 +1144,7 @@ public class Paint_Delegate {
                    info.mFont = info.mFont.deriveFont(new AffineTransform(
                            mTextScaleX, mTextSkewX, 0, 1, 0, 0));
                }
                // The metrics here don't have anti-aliasing set.
                info.mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(info.mFont);

                infoList.add(info);
@@ -1185,64 +1154,9 @@ public class Paint_Delegate {
        }
    }

    /*package*/ float measureText(char[] text, int index, int count, int bidiFlags) {
        // TODO: find out what bidiFlags actually does.

        // WARNING: the logic in this method is similar to Canvas_Delegate.native_drawText
        // Any change to this method should be reflected there as well

        if (mFonts.size() > 0) {
            FontInfo mainFont = mFonts.get(0);
            int i = index;
            int lastIndex = index + count;
            float total = 0f;
            while (i < lastIndex) {
                // always start with the main font.
                int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex);
                if (upTo == -1) {
                    // shortcut to exit
                    return total + mainFont.mMetrics.charsWidth(text, i, lastIndex - i);
                } else if (upTo > 0) {
                    total += mainFont.mMetrics.charsWidth(text, i, upTo - i);
                    i = upTo;
                    // don't call continue at this point. Since it is certain the main font
                    // cannot display the font a index upTo (now ==i), we move on to the
                    // fallback fonts directly.
                }

                // no char supported, attempt to read the next char(s) with the
                // fallback font. In this case we only test the first character
                // and then go back to test with the main font.
                // Special test for 2-char characters.
                boolean foundFont = false;
                for (int f = 1 ; f < mFonts.size() ; f++) {
                    FontInfo fontInfo = mFonts.get(f);

                    // need to check that the font can display the character. We test
                    // differently if the char is a high surrogate.
                    int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
                    upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount);
                    if (upTo == -1) {
                        total += fontInfo.mMetrics.charsWidth(text, i, charCount);
                        i += charCount;
                        foundFont = true;
                        break;

                    }
                }

                // in case no font can display the char, measure it with the main font.
                if (foundFont == false) {
                    int size = Character.isHighSurrogate(text[i]) ? 2 : 1;
                    total += mainFont.mMetrics.charsWidth(text, i, size);
                    i += size;
                }
            }

            return total;
        }

        return 0;
    /*package*/ float measureText(char[] text, int index, int count, boolean isRtl) {
        return new BidiRenderer(null, this, text).renderText(
                index, index + count, isRtl, null, 0, false, 0, 0);
    }

    private float getFontMetrics(FontMetrics metrics) {
@@ -1281,4 +1195,14 @@ public class Paint_Delegate {
        }
    }

    private static boolean isRtl(int flag) {
        switch(flag) {
        case Paint.BIDI_RTL:
        case Paint.BIDI_FORCE_RTL:
        case Paint.BIDI_DEFAULT_RTL:
            return true;
        default:
            return false;
        }
    }
}
+27 −4
Original line number Diff line number Diff line
@@ -16,7 +16,10 @@

package android.text;

import com.android.ide.common.rendering.api.LayoutLog;
import com.android.layoutlib.bridge.Bridge;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import com.ibm.icu.text.Bidi;


/**
@@ -29,9 +32,29 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
public class AndroidBidi_Delegate {

    @LayoutlibDelegate
    /*package*/ static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo) {
        // return the equivalent of Layout.DIR_LEFT_TO_RIGHT
        // TODO: actually figure the direction.
        return 0;
    /*package*/ static int runBidi(int dir, char[] chars, byte[] charInfo, int count,
            boolean haveInfo) {

        switch (dir) {
        case 0: // Layout.DIR_REQUEST_LTR
        case 1: // Layout.DIR_REQUEST_RTL
            break;  // No change.
        case -1:
            dir = Bidi.LEVEL_DEFAULT_LTR;
            break;
        case -2:
            dir = Bidi.LEVEL_DEFAULT_RTL;
            break;
        default:
            // Invalid code. Log error, assume LEVEL_DEFAULT_LTR and continue.
            Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Invalid direction flag", null);
            dir = Bidi.LEVEL_DEFAULT_LTR;
        }
        Bidi bidi = new Bidi(chars, 0, null, 0, count, dir);
        if (charInfo != null) {
            for (int i = 0; i < count; ++i)
            charInfo[i] = bidi.getLevelAt(i);
        }
        return bidi.getParaLevel();
    }
}
+20 −2

File changed.

Preview size limit exceeded, changes collapsed.