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

Commit 0d9c922c authored by Deepanshu Gupta's avatar Deepanshu Gupta
Browse files

Fix text rendering

There are still some errors
1. Little vertical clippping for extra tall glyphs.
2. Breaking into scripts isn't perfect which results in incorrect layout
of text.

Change-Id: I54de3c05eca5e8affb1135c120eea24c3afe8a47
parent 88db0ee2
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.