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

Commit f3a1915b authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Add TextShaper API

TextShaper API provides a primitive text layout result, e.g. what glyph is used etc.

Here is the new APIs and its description

- PositionedGlyphs
This is a native instance backed object that gives layout information to developers.

- GlyphStyle
The glyph style is an object that holds the parameters that will be used for drawing.
This object is a subset of the Paint object for avoiding creating lots of Paint copy.

- TextShaper#shapeText
This does a text shaping and gives TextShaper.Result. This is a native backed primitive
shaping API.

- StyledTextShaper
This is a text shaper for a styled text. This will be the top-level developer facing
API for shaping text.

Bug: 168048923
Test: atest FontTest TextShaperTest StyledTextShaperTest
Change-Id: I2e91d1ef8503b25b28efc94da7de7cad49c4c1a9
parent 08348e26
Loading
Loading
Loading
Loading
+40 −0
Original line number Diff line number Diff line
@@ -16508,6 +16508,23 @@ package android.graphics.pdf {
package android.graphics.text {
  public class GlyphStyle {
    ctor public GlyphStyle(@ColorInt int, @FloatRange(from=0) float, @FloatRange(from=0) float, @FloatRange(from=0) float, int);
    ctor public GlyphStyle(@NonNull android.graphics.Paint);
    method public void applyToPaint(@NonNull android.graphics.Paint);
    method @ColorInt public int getColor();
    method public int getFlags();
    method @FloatRange(from=0) public float getFontSize();
    method @FloatRange(from=0) public float getScaleX();
    method @FloatRange(from=0) public float getSkewX();
    method public void setColor(@ColorInt int);
    method public void setFlags(int);
    method public void setFontSize(@FloatRange(from=0) float);
    method public void setFromPaint(@NonNull android.graphics.Paint);
    method public void setScaleX(@FloatRange(from=0) float);
    method public void setSkewX(@FloatRange(from=0) float);
  }
  public class LineBreaker {
    method @NonNull public android.graphics.text.LineBreaker.Result computeLineBreaks(@NonNull android.graphics.text.MeasuredText, @NonNull android.graphics.text.LineBreaker.ParagraphConstraints, @IntRange(from=0) int);
    field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2
@@ -16568,6 +16585,25 @@ package android.graphics.text {
    method @NonNull public android.graphics.text.MeasuredText.Builder setComputeLayout(boolean);
  }
  public final class PositionedGlyphs {
    method public float getAscent();
    method public float getDescent();
    method @NonNull public android.graphics.fonts.Font getFont(@IntRange(from=0) int);
    method @IntRange(from=0) public int getGlyphId(@IntRange(from=0) int);
    method public float getOriginX();
    method public float getOriginY();
    method public float getPositionX(@IntRange(from=0) int);
    method public float getPositionY(@IntRange(from=0) int);
    method @NonNull public android.graphics.text.GlyphStyle getStyle();
    method public float getTotalAdvance();
    method @IntRange(from=0) public int glyphCount();
  }
  public class TextShaper {
    method @NonNull public static android.graphics.text.PositionedGlyphs shapeTextRun(@NonNull char[], int, int, int, int, float, float, boolean, @NonNull android.graphics.Paint);
    method @NonNull public static android.graphics.text.PositionedGlyphs shapeTextRun(@NonNull CharSequence, int, int, int, int, float, float, boolean, @NonNull android.graphics.Paint);
  }
}
package android.hardware {
@@ -49990,6 +50026,10 @@ package android.text {
    method @NonNull public android.text.StaticLayout.Builder setUseLineSpacingFromFallbacks(boolean);
  }
  public class StyledTextShaper {
    method @NonNull public static java.util.List<android.graphics.text.PositionedGlyphs> shapeText(@NonNull CharSequence, int, int, @NonNull android.text.TextDirectionHeuristic, @NonNull android.text.TextPaint);
  }
  public interface TextDirectionHeuristic {
    method public boolean isRtl(char[], int, int);
    method public boolean isRtl(CharSequence, int, int);
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.text;

import android.annotation.NonNull;
import android.graphics.Paint;
import android.graphics.text.PositionedGlyphs;
import android.graphics.text.TextShaper;

import java.util.List;

/**
 * Provides text shaping for multi-styled text.
 *
 * @see TextShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint)
 * @see TextShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)
 * @see StyledTextShaper#shapeText(CharSequence, int, int, TextDirectionHeuristic, TextPaint)
 */
public class StyledTextShaper {
    private StyledTextShaper() {}


    /**
     * Shape multi-styled text.
     *
     * @param text a styled text.
     * @param start a start index of shaping target in the text.
     * @param count a length of shaping target in the text.
     * @param dir a text direction.
     * @param paint a paint
     * @return a shape result.
     */
    public static @NonNull List<PositionedGlyphs> shapeText(
            @NonNull CharSequence text, int start, int count,
            @NonNull TextDirectionHeuristic dir, @NonNull TextPaint paint) {
        MeasuredParagraph mp = MeasuredParagraph.buildForBidi(
                text, start, start + count, dir, null);
        TextLine tl = TextLine.obtain();
        try {
            tl.set(paint, text, start, start + count,
                    mp.getParagraphDir(),
                    mp.getDirections(start, start + count),
                    false /* tabstop is not supported */,
                    null,
                    -1, -1 // ellipsis is not supported.
            );
            return tl.shape();
        } finally {
            TextLine.recycle(tl);
        }
    }

}
+122 −18
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.text.PositionedGlyphs;
import android.graphics.text.TextShaper;
import android.os.Build;
import android.text.Layout.Directions;
import android.text.Layout.TabStops;
@@ -35,6 +37,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * Represents a line of styled text, for measuring in visual order and
@@ -306,6 +309,36 @@ public class TextLine {
        return measure(mLen, false, fmi);
    }

    /**
     * Shape the TextLine.
     */
    List<PositionedGlyphs> shape() {
        List<PositionedGlyphs> glyphs = new ArrayList<>();
        float horizontal = 0;
        float x = 0;
        final int runCount = mDirections.getRunCount();
        for (int runIndex = 0; runIndex < runCount; runIndex++) {
            final int runStart = mDirections.getRunStart(runIndex);
            if (runStart > mLen) break;
            final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
            final boolean runIsRtl = mDirections.isRunRtl(runIndex);

            int segStart = runStart;
            for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
                if (j == runLimit || charAt(j) == TAB_CHAR) {
                    horizontal += shapeRun(glyphs, segStart, j, runIsRtl, x + horizontal,
                            runIndex != (runCount - 1) || j != mLen);

                    if (j != runLimit) {  // charAt(j) == TAB_CHAR
                        horizontal = mDir * nextTab(horizontal * mDir);
                    }
                    segStart = j + 1;
                }
            }
        }
        return glyphs;
    }

    /**
     * Returns the signed graphical offset from the leading margin.
     *
@@ -483,12 +516,12 @@ public class TextLine {

        if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
            float w = -measureRun(start, limit, limit, runIsRtl, null);
            handleRun(start, limit, limit, runIsRtl, c, x + w, top,
            handleRun(start, limit, limit, runIsRtl, c, null, x + w, top,
                    y, bottom, null, false);
            return w;
        }

        return handleRun(start, limit, limit, runIsRtl, c, x, top,
        return handleRun(start, limit, limit, runIsRtl, c, null, x, top,
                y, bottom, null, needWidth);
    }

@@ -507,9 +540,34 @@ public class TextLine {
     */
    private float measureRun(int start, int offset, int limit, boolean runIsRtl,
            FontMetricsInt fmi) {
        return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true);
        return handleRun(start, offset, limit, runIsRtl, null, null, 0, 0, 0, 0, fmi, true);
    }

    /**
     * Shape a unidirectional (but possibly multi-styled) run of text.
     *
     * @param glyphs the output positioned glyphs list
     * @param start the line-relative start
     * @param limit the line-relative limit
     * @param runIsRtl true if the run is right-to-left
     * @param x the position of the run that is closest to the leading margin
     * @param needWidth true if the width value is required.
     * @return the signed width of the run, based on the paragraph direction.
     * Only valid if needWidth is true.
     */
    private float shapeRun(List<PositionedGlyphs> glyphs, int start,
            int limit, boolean runIsRtl, float x, boolean needWidth) {

        if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
            float w = -measureRun(start, limit, limit, runIsRtl, null);
            handleRun(start, limit, limit, runIsRtl, null, glyphs, x + w, 0, 0, 0, null, false);
            return w;
        }

        return handleRun(start, limit, limit, runIsRtl, null, glyphs, x, 0, 0, 0, null, needWidth);
    }


    /**
     * Walk the cursor through this line, skipping conjuncts and
     * zero-width characters.
@@ -841,6 +899,7 @@ public class TextLine {
     * @param end the end of the text
     * @param runIsRtl true if the run is right-to-left
     * @param c the canvas, can be null if rendering is not needed
     * @param glyphs the output positioned glyph list, can be null if not necessary
     * @param x the edge of the run closest to the leading margin
     * @param top the top of the line
     * @param y the baseline
@@ -854,7 +913,7 @@ public class TextLine {
     */
    private float handleText(TextPaint wp, int start, int end,
            int contextStart, int contextEnd, boolean runIsRtl,
            Canvas c, float x, int top, int y, int bottom,
            Canvas c, List<PositionedGlyphs> glyphs, float x, int top, int y, int bottom,
            FontMetricsInt fmi, boolean needWidth, int offset,
            @Nullable ArrayList<DecorationInfo> decorations) {

@@ -878,7 +937,6 @@ public class TextLine {
            totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
        }

        if (c != null) {
        final float leftX, rightX;
        if (runIsRtl) {
            leftX = x - totalWidth;
@@ -888,6 +946,11 @@ public class TextLine {
            rightX = x + totalWidth;
        }

        if (glyphs != null) {
            shapeTextRun(glyphs, wp, start, end, contextStart, contextEnd, runIsRtl, leftX);
        }

        if (c != null) {
            if (wp.bgColor != 0) {
                int previousColor = wp.getColor();
                Paint.Style previousStyle = wp.getStyle();
@@ -1072,6 +1135,7 @@ public class TextLine {
     * @param limit the limit of the run
     * @param runIsRtl true if the run is right-to-left
     * @param c the canvas, can be null
     * @param glyphs the output positioned glyphs, can be null
     * @param x the end of the run closest to the leading margin
     * @param top the top of the line
     * @param y the baseline
@@ -1082,7 +1146,8 @@ public class TextLine {
     * valid if needWidth is true
     */
    private float handleRun(int start, int measureLimit,
            int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
            int limit, boolean runIsRtl, Canvas c,
            List<PositionedGlyphs> glyphs, float x, int top, int y,
            int bottom, FontMetricsInt fmi, boolean needWidth) {

        if (measureLimit < start || measureLimit > limit) {
@@ -1115,7 +1180,7 @@ public class TextLine {
            wp.set(mPaint);
            wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
            wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
            return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
            return handleText(wp, start, limit, start, limit, runIsRtl, c, glyphs, x, top,
                    y, bottom, fmi, needWidth, measureLimit, null);
        }

@@ -1196,8 +1261,8 @@ public class TextLine {
                            adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
                    activePaint.setEndHyphenEdit(
                            adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
                            top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
                    x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c,
                            glyphs, x, top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
                            Math.min(activeEnd, mlimit), mDecorations);

                    activeStart = j;
@@ -1223,7 +1288,7 @@ public class TextLine {
                    adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
            activePaint.setEndHyphenEdit(
                    adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
            x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, glyphs, x,
                    top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
                    Math.min(activeEnd, mlimit), mDecorations);
        }
@@ -1259,6 +1324,45 @@ public class TextLine {
        }
    }

    /**
     * Shape a text run with the set-up paint.
     *
     * @param glyphs the output positioned glyphs list
     * @param paint the paint used to render the text
     * @param start the start of the run
     * @param end the end of the run
     * @param contextStart the start of context for the run
     * @param contextEnd the end of the context for the run
     * @param runIsRtl true if the run is right-to-left
     * @param x the x position of the left edge of the run
     */
    private void shapeTextRun(List<PositionedGlyphs> glyphs, TextPaint paint,
            int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x) {

        int count = end - start;
        int contextCount = contextEnd - contextStart;
        if (mCharsValid) {
            glyphs.add(TextShaper.shapeTextRun(
                    mChars,
                    start, count,
                    contextStart, contextCount,
                    x, 0f,
                    runIsRtl,
                    paint
            ));
        } else {
            glyphs.add(TextShaper.shapeTextRun(
                    mText,
                    mStart + start, count,
                    mStart + contextStart, contextCount,
                    x, 0f,
                    runIsRtl,
                    paint
            ));
        }
    }


    /**
     * Returns the next tab position.
     *
+74 −0
Original line number Diff line number Diff line
@@ -26,8 +26,11 @@ import android.graphics.Paint;
import android.graphics.RectF;
import android.os.LocaleList;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.TypedValue;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import dalvik.annotation.optimization.CriticalNative;
@@ -40,6 +43,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
@@ -56,6 +60,15 @@ public final class Font {
    private static final int STYLE_ITALIC = 1;
    private static final int STYLE_NORMAL = 0;

    private static final Object MAP_LOCK = new Object();
    // We need to have mapping from native ptr to Font object for later accessing from TextShape
    // result since Typeface doesn't have reference to Font object and it is not always created from
    // Font object. Sometimes Typeface is created in native layer only and there might not be Font
    // object in Java layer. So, if not found in this cache, create new Font object for API user.
    @GuardedBy("MAP_LOCK")
    private static final LongSparseArray<WeakReference<Font>> FONT_PTR_MAP =
            new LongSparseArray<>();

    /**
     * A builder class for creating new Font.
     */
@@ -501,6 +514,10 @@ public final class Font {
        mTtcIndex = ttcIndex;
        mAxes = axes;
        mLocaleList = localeList;

        synchronized (MAP_LOCK) {
            FONT_PTR_MAP.append(mNativePtr, new WeakReference<>(this));
        }
    }

    /**
@@ -637,6 +654,63 @@ public final class Font {
            + "}";
    }

    /**
     * Lookup Font object from native pointer or create new one if not found.
     * @hide
     */
    public static Font findOrCreateFontFromNativePtr(long ptr) {
        // First, lookup from known mapps.
        synchronized (MAP_LOCK) {
            WeakReference<Font> fontRef = FONT_PTR_MAP.get(ptr);
            if (fontRef != null) {
                Font font = fontRef.get();
                if (font != null) {
                    return font;
                }
            }

            // If not found, create Font object from native object for Java API users.
            ByteBuffer buffer = NativeFontBufferHelper.refByteBuffer(ptr);
            long packed = nGetFontInfo(ptr);
            int weight = (int) (packed & 0x0000_0000_0000_FFFFL);
            boolean italic = (packed & 0x0000_0000_0001_0000L) != 0;
            int ttcIndex = (int) ((packed & 0x0000_FFFF_0000_0000L) >> 32);
            int axisCount = (int) ((packed & 0xFFFF_0000_0000_0000L) >> 48);
            FontVariationAxis[] axes = new FontVariationAxis[axisCount];
            char[] charBuffer = new char[4];
            for (int i = 0; i < axisCount; ++i) {
                long packedAxis = nGetAxisInfo(ptr, i);
                float value = Float.intBitsToFloat((int) (packedAxis & 0x0000_0000_FFFF_FFFFL));
                charBuffer[0] = (char) ((packedAxis & 0xFF00_0000_0000_0000L) >> 56);
                charBuffer[1] = (char) ((packedAxis & 0x00FF_0000_0000_0000L) >> 48);
                charBuffer[2] = (char) ((packedAxis & 0x0000_FF00_0000_0000L) >> 40);
                charBuffer[3] = (char) ((packedAxis & 0x0000_00FF_0000_0000L) >> 32);
                axes[i] = new FontVariationAxis(new String(charBuffer), value);
            }
            Font.Builder builder = new Font.Builder(buffer)
                    .setWeight(weight)
                    .setSlant(italic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT)
                    .setTtcIndex(ttcIndex)
                    .setFontVariationSettings(axes);

            Font newFont = null;
            try {
                newFont = builder.build();
                FONT_PTR_MAP.append(ptr, new WeakReference<>(newFont));
            } catch (IOException e) {
                // This must not happen since the buffer was already created once.
                Log.e("Font", "Failed to create font object from existing buffer.", e);
            }
            return newFont;
        }
    }

    @CriticalNative
    private static native long nGetFontInfo(long ptr);

    @CriticalNative
    private static native long nGetAxisInfo(long ptr, int i);

    @FastNative
    private static native float nGetGlyphBounds(long font, int glyphId, long paint, RectF rect);

+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.fonts;

import android.annotation.NonNull;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;

import libcore.util.NativeAllocationRegistry;

import java.nio.ByteBuffer;

/**
 * This is a helper class for showing native allocated buffer in Java API.
 *
 * @hide
 */
public class NativeFontBufferHelper {
    private NativeFontBufferHelper() {}

    private static final NativeAllocationRegistry REGISTRY =
            NativeAllocationRegistry.createMalloced(
                    ByteBuffer.class.getClassLoader(), nGetReleaseFunc());

    /**
     * Wrap native buffer with ByteBuffer with adding reference to it.
     */
    public static @NonNull ByteBuffer refByteBuffer(long fontPtr) {
        long refPtr = nRefFontBuffer(fontPtr);
        ByteBuffer buffer = nWrapByteBuffer(refPtr);

        // Releasing native object so that decreasing shared pointer ref count when the byte buffer
        // is GCed.
        REGISTRY.registerNativeAllocation(buffer, refPtr);

        return buffer;
    }

    @CriticalNative
    private static native long nRefFontBuffer(long fontPtr);

    @FastNative
    private static native ByteBuffer nWrapByteBuffer(long refPtr);

    @CriticalNative
    private static native long nGetReleaseFunc();
}
Loading