diff options
| -rw-r--r-- | api/current.txt | 40 | ||||
| -rw-r--r-- | core/java/android/text/StyledTextShaper.java | 67 | ||||
| -rw-r--r-- | core/java/android/text/TextLine.java | 140 | ||||
| -rw-r--r-- | graphics/java/android/graphics/fonts/Font.java | 74 | ||||
| -rw-r--r-- | graphics/java/android/graphics/fonts/NativeFontBufferHelper.java | 62 | ||||
| -rw-r--r-- | graphics/java/android/graphics/text/GlyphStyle.java | 234 | ||||
| -rw-r--r-- | graphics/java/android/graphics/text/PositionedGlyphs.java | 277 | ||||
| -rw-r--r-- | graphics/java/android/graphics/text/TextShaper.java | 125 | ||||
| -rw-r--r-- | libs/hwui/Android.bp | 1 | ||||
| -rw-r--r-- | libs/hwui/apex/jni_runtime.cpp | 2 | ||||
| -rw-r--r-- | libs/hwui/hwui/MinikinUtils.h | 2 | ||||
| -rw-r--r-- | libs/hwui/jni/FontUtils.h | 8 | ||||
| -rw-r--r-- | libs/hwui/jni/fonts/Font.cpp | 65 | ||||
| -rw-r--r-- | libs/hwui/jni/text/TextShaper.cpp | 206 | ||||
| -rw-r--r-- | non-updatable-api/current.txt | 40 |
15 files changed, 1323 insertions, 20 deletions
diff --git a/api/current.txt b/api/current.txt index 4b0901a122e7..6f897cbdceb8 100644 --- a/api/current.txt +++ b/api/current.txt @@ -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); diff --git a/core/java/android/text/StyledTextShaper.java b/core/java/android/text/StyledTextShaper.java new file mode 100644 index 000000000000..bf906143bc56 --- /dev/null +++ b/core/java/android/text/StyledTextShaper.java @@ -0,0 +1,67 @@ +/* + * 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); + } + } + +} diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 3c51fa765263..b82683260985 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -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 @@ -307,6 +310,36 @@ public class TextLine { } /** + * 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. * * Following examples are all for measuring offset=3. LX(e.g. L0, L1, ...) denotes a @@ -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,16 +937,20 @@ public class TextLine { totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset); } - if (c != null) { - final float leftX, rightX; - if (runIsRtl) { - leftX = x - totalWidth; - rightX = x; - } else { - leftX = x; - rightX = x + totalWidth; - } + final float leftX, rightX; + if (runIsRtl) { + leftX = x - totalWidth; + rightX = x; + } else { + leftX = x; + 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); } @@ -1260,6 +1325,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. * * @param h the (unsigned) offset from the leading margin diff --git a/graphics/java/android/graphics/fonts/Font.java b/graphics/java/android/graphics/fonts/Font.java index cbae67507a64..2a52ce962c83 100644 --- a/graphics/java/android/graphics/fonts/Font.java +++ b/graphics/java/android/graphics/fonts/Font.java @@ -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); diff --git a/graphics/java/android/graphics/fonts/NativeFontBufferHelper.java b/graphics/java/android/graphics/fonts/NativeFontBufferHelper.java new file mode 100644 index 000000000000..5655e7fafc1b --- /dev/null +++ b/graphics/java/android/graphics/fonts/NativeFontBufferHelper.java @@ -0,0 +1,62 @@ +/* + * 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(); +} diff --git a/graphics/java/android/graphics/text/GlyphStyle.java b/graphics/java/android/graphics/text/GlyphStyle.java new file mode 100644 index 000000000000..cc8c4d26fb5e --- /dev/null +++ b/graphics/java/android/graphics/text/GlyphStyle.java @@ -0,0 +1,234 @@ +/* + * 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.text; + +import android.annotation.ColorInt; +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.graphics.Paint; + +import java.util.Objects; + +/** + * Represents subset of Paint parameters such as font size, scaleX that is used to draw a glyph. + * + * Glyph is a most primitive unit of text drawing. + * + */ +public class GlyphStyle { + private @ColorInt int mColor; + private float mFontSize; + private float mScaleX; + private float mSkewX; + private int mFlags; + + /** + * @param color a color. + * @param fontSize a font size in pixels. + * @param scaleX a horizontal scale factor. + * @param skewX a horizontal skew factor + * @param flags paint flags + * + * @see Paint#getFlags() + * @see Paint#setFlags(int) + */ + public GlyphStyle( + @ColorInt int color, + @FloatRange(from = 0) float fontSize, + @FloatRange(from = 0) float scaleX, + @FloatRange(from = 0) float skewX, + int flags) { + mColor = color; + mFontSize = fontSize; + mScaleX = scaleX; + mSkewX = skewX; + mFlags = flags; + } + + /** + * Create glyph style from Paint + * + * @param paint a paint + */ + public GlyphStyle(@NonNull Paint paint) { + setFromPaint(paint); + } + + /** + * Gets the color. + * + * @return a color + * @see Paint#getColor() + * @see Paint#setColor(int) + */ + public @ColorInt int getColor() { + return mColor; + } + + /** + * Sets the color. + * + * @param color a color + * @see Paint#getColor() + * @see Paint#setColor(int) + */ + public void setColor(@ColorInt int color) { + mColor = color; + } + + /** + * Gets the font size in pixels. + * + * @return font size + * @see Paint#getTextSize() + * @see Paint#setTextSize(float) + */ + public @FloatRange(from = 0) float getFontSize() { + return mFontSize; + } + + /** + * Sets the font size in pixels. + * + * @param fontSize font size in pixel + * @see Paint#getTextSize() + * @see Paint#setTextSize(float) + */ + public void setFontSize(@FloatRange(from = 0) float fontSize) { + mFontSize = fontSize; + } + + /** + * Return the horizontal scale factor + * + * @return a horizontal scale factor + * @see Paint#getTextScaleX() + * @see Paint#setTextScaleX(float) + */ + public @FloatRange(from = 0) float getScaleX() { + return mScaleX; + } + + /** + * Set the horizontal scale factor + * + * @param scaleX a horizontal scale factor + * @see Paint#getTextScaleX() + * @see Paint#setTextScaleX(float) + */ + public void setScaleX(@FloatRange(from = 0) float scaleX) { + mScaleX = scaleX; + } + + /** + * Return the horizontal skew factor + * + * @return a horizontal skew factor + * @see Paint#getTextSkewX() + * @see Paint#setTextSkewX(float) + */ + public @FloatRange(from = 0) float getSkewX() { + return mSkewX; + } + + /** + * Set the horizontal skew factor + * + * @param skewX a horizontal skew factor + * @see Paint#getTextSkewX() + * @see Paint#setTextSkewX(float) + */ + public void setSkewX(@FloatRange(from = 0) float skewX) { + mSkewX = skewX; + } + + /** + * Returns the Paint flags. + * + * @return a paint flags + * @see Paint#getFlags() + * @see Paint#setFlags(int) + */ + public int getFlags() { + return mFlags; + } + + /** + * Set the Paint flags. + * + * @param flags a paint flags + * @see Paint#getFlags() + * @see Paint#setFlags(int) + */ + public void setFlags(int flags) { + mFlags = flags; + } + + /** + * Applies glyph style to the paint object. + * + * @param paint a paint object + */ + public void applyToPaint(@NonNull Paint paint) { + paint.setColor(mColor); + paint.setTextSize(mFontSize); + paint.setTextScaleX(mScaleX); + paint.setTextSkewX(mSkewX); + paint.setFlags(mFlags); + } + + /** + * Copy parameters from a Paint object. + * + * @param paint a paint object + */ + public void setFromPaint(@NonNull Paint paint) { + mColor = paint.getColor(); + mFontSize = paint.getTextSize(); + mScaleX = paint.getTextScaleX(); + mSkewX = paint.getTextSkewX(); + mFlags = paint.getFlags(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GlyphStyle)) return false; + GlyphStyle that = (GlyphStyle) o; + return that.mColor == mColor + && Float.compare(that.mFontSize, mFontSize) == 0 + && Float.compare(that.mScaleX, mScaleX) == 0 + && Float.compare(that.mSkewX, mSkewX) == 0 + && mFlags == that.mFlags; + } + + @Override + public int hashCode() { + return Objects.hash(mColor, mFontSize, mScaleX, mSkewX, mFlags); + } + + @Override + public String toString() { + return "GlyphStyle{" + + "mColor=" + mColor + + ", mFontSize=" + mFontSize + + ", mScaleX=" + mScaleX + + ", mSkewX=" + mSkewX + + ", mFlags=" + mFlags + + '}'; + } +} diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java new file mode 100644 index 000000000000..7364d545a452 --- /dev/null +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -0,0 +1,277 @@ +/* + * 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.text; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.graphics.fonts.Font; + +import com.android.internal.util.Preconditions; + +import dalvik.annotation.optimization.CriticalNative; + +import libcore.util.NativeAllocationRegistry; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * Text shaping result object for single style text. + * + * You can get text shaping result by + * {@link TextShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint)} and + * {@link TextShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)}. + * + * @see TextShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint) + * @see TextShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint) + */ +public final class PositionedGlyphs { + private static final NativeAllocationRegistry REGISTRY = + NativeAllocationRegistry.createMalloced( + Typeface.class.getClassLoader(), nReleaseFunc()); + + private final long mLayoutPtr; + private final float mXOffset; + private final float mYOffset; + private final GlyphStyle mGlyphStyle; + private final ArrayList<Font> mFonts; + + /** + * Returns the total amount of advance consumed by this positioned glyphs. + * + * The advance is an amount of width consumed by the glyph. The total amount of advance is + * a total amount of advance consumed by this series of glyphs. In other words, if another + * glyph is placed next to this series of glyphs, it's X offset should be shifted this amount + * of width. + * + * @return total amount of advance + */ + public float getTotalAdvance() { + return nGetTotalAdvance(mLayoutPtr); + } + + /** + * Effective ascent value of this positioned glyphs. + * + * If two or more font files are used in this series of glyphs, the effective ascent will be + * the minimum ascent value across the all font files. + * + * @return effective ascent value + */ + public float getAscent() { + return nGetAscent(mLayoutPtr); + } + + /** + * Effective descent value of this positioned glyphs. + * + * If two or more font files are used in this series of glyphs, the effective descent will be + * the maximum descent value across the all font files. + * + * @return effective descent value + */ + public float getDescent() { + return nGetDescent(mLayoutPtr); + } + + /** + * Returns the glyph style used for drawing the glyph at the given index. + * + * @return A glyph style + */ + @NonNull + public GlyphStyle getStyle() { + return mGlyphStyle; + } + + /** + * Returns the amount of X offset added to glyph position. + * + * @return The X offset added to glyph position. + */ + public float getOriginX() { + return mXOffset; + } + + /** + * Returns the amount of Y offset added to glyph position. + * + * @return The Y offset added to glyph position. + */ + public float getOriginY() { + return mYOffset; + } + + /** + * Returns the number of glyphs stored. + * + * @return the number of glyphs + */ + @IntRange(from = 0) + public int glyphCount() { + return nGetGlyphCount(mLayoutPtr); + } + + /** + * Returns the font object used for drawing the glyph at the given index. + * + * @param index the glyph index + * @return the font object used for drawing the glyph at the given index + */ + @NonNull + public Font getFont(@IntRange(from = 0) int index) { + Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); + return mFonts.get(index); + } + + /** + * Returns the glyph ID used for drawing the glyph at the given index. + * + * @param index the glyph index + * @return A font object + */ + @IntRange(from = 0) + public int getGlyphId(@IntRange(from = 0) int index) { + Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); + return nGetGlyphId(mLayoutPtr, index); + } + + /** + * Returns the x coordinate of the glyph position at the given index. + * + * @param index the glyph index + * @return A X offset in pixels + */ + public float getPositionX(@IntRange(from = 0) int index) { + Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); + return nGetX(mLayoutPtr, index) + mXOffset; + } + + /** + * Returns the y coordinate of the glyph position at the given index. + * + * @param index the glyph index + * @return A Y offset in pixels. + */ + public float getPositionY(@IntRange(from = 0) int index) { + Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); + return nGetY(mLayoutPtr, index) + mYOffset; + } + + /** + * Create single style layout from native result. + * + * @hide + * + * @param layoutPtr the address of native layout object. + * @param paint a paint object + */ + public PositionedGlyphs(long layoutPtr, @NonNull Paint paint, float xOffset, float yOffset) { + mLayoutPtr = layoutPtr; + mGlyphStyle = new GlyphStyle(paint); + int glyphCount = nGetGlyphCount(layoutPtr); + mFonts = new ArrayList<>(glyphCount); + mXOffset = xOffset; + mYOffset = yOffset; + + long prevPtr = 0; + Font prevFont = null; + for (int i = 0; i < glyphCount; ++i) { + long ptr = nGetFont(layoutPtr, i); + if (prevPtr != ptr) { + prevPtr = ptr; + prevFont = Font.findOrCreateFontFromNativePtr(ptr); + } + mFonts.add(prevFont); + } + + REGISTRY.registerNativeAllocation(this, layoutPtr); + } + + @CriticalNative + private static native int nGetGlyphCount(long minikinLayout); + @CriticalNative + private static native float nGetTotalAdvance(long minikinLayout); + @CriticalNative + private static native float nGetAscent(long minikinLayout); + @CriticalNative + private static native float nGetDescent(long minikinLayout); + @CriticalNative + private static native int nGetGlyphId(long minikinLayout, int i); + @CriticalNative + private static native float nGetX(long minikinLayout, int i); + @CriticalNative + private static native float nGetY(long minikinLayout, int i); + @CriticalNative + private static native long nGetFont(long minikinLayout, int i); + @CriticalNative + private static native long nReleaseFunc(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PositionedGlyphs)) return false; + PositionedGlyphs that = (PositionedGlyphs) o; + + if (!mGlyphStyle.equals(that.mGlyphStyle)) return false; + if (mXOffset != that.mXOffset || mYOffset != that.mYOffset) return false; + if (glyphCount() != that.glyphCount()) return false; + + for (int i = 0; i < glyphCount(); ++i) { + if (getGlyphId(i) != that.getGlyphId(i)) return false; + if (getPositionX(i) != that.getPositionX(i)) return false; + if (getPositionY(i) != that.getPositionY(i)) return false; + // Intentionally using reference equality since font equality is heavy due to buffer + // compare. + if (getFont(i) != that.getFont(i)) return false; + } + + return true; + } + + @Override + public int hashCode() { + int hashCode = Objects.hash(mXOffset, mYOffset, mGlyphStyle); + for (int i = 0; i < glyphCount(); ++i) { + hashCode = Objects.hash(hashCode, + getGlyphId(i), getPositionX(i), getPositionY(i), getFont(i)); + } + return hashCode; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < glyphCount(); ++i) { + if (i != 0) { + sb.append(", "); + } + sb.append("[ ID = " + getGlyphId(i) + "," + + " pos = (" + getPositionX(i) + "," + getPositionY(i) + ")" + + " font = " + getFont(i) + " ]"); + } + sb.append("]"); + return "PositionedGlyphs{" + + "glyphs = " + sb.toString() + + ", mXOffset=" + mXOffset + + ", mYOffset=" + mYOffset + + ", mGlyphStyle=" + mGlyphStyle + + '}'; + } +} diff --git a/graphics/java/android/graphics/text/TextShaper.java b/graphics/java/android/graphics/text/TextShaper.java new file mode 100644 index 000000000000..f40ed8f8f653 --- /dev/null +++ b/graphics/java/android/graphics/text/TextShaper.java @@ -0,0 +1,125 @@ +/* + * 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.text; + +import android.annotation.NonNull; +import android.graphics.Paint; +import android.text.TextDirectionHeuristic; +import android.text.TextUtils; + +import com.android.internal.util.Preconditions; + +import dalvik.annotation.optimization.FastNative; + +/** + * Provides conversion from a text into glyph array. + * + * Text shaping is a preprocess for drawing text into canvas with glyphs. The glyph is a most + * primitive unit of the text drawing, consist of glyph identifier in the font file and its position + * and style. You can draw the shape result to Canvas by calling Canvas#drawGlyphs. + + * + * @see TextShaper#shapeTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint) + * @see TextShaper#shapeTextRun(char[], int, int, int, int, float, float, boolean, Paint) + * @see android.text.StyledTextShaper#shapeText(CharSequence, int, int, TextDirectionHeuristic, + * TextPaint) + */ +public class TextShaper { + private TextShaper() {} // Do not instantiate + + /** + * Shape non-styled text. + * + * This function shapes the text of the given range under the context of given context range. + * Some script, e.g. Arabic or Devanagari, changes letter shape based on its location or + * surrounding characters. + * + * @param text a text buffer to be shaped + * @param start a start index of shaping target in the buffer. + * @param count a length of shaping target in the buffer. + * @param contextStart a start index of context used for shaping in the buffer. + * @param contextCount a length of context used for shaping in the buffer. + * @param xOffset an additional amount of x offset of the result glyphs. + * @param yOffset an additional amount of y offset of the result glyphs. + * @param isRtl true if this text is shaped for RTL direction, false otherwise. + * @param paint a paint used for shaping text. + * @return a shape result. + */ + @NonNull + public static PositionedGlyphs shapeTextRun( + @NonNull char[] text, int start, int count, int contextStart, int contextCount, + float xOffset, float yOffset, boolean isRtl, @NonNull Paint paint) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(paint); + return new PositionedGlyphs( + nativeShapeTextRun(text, start, count, contextStart, contextCount, isRtl, + paint.getNativeInstance()), + paint, xOffset, yOffset); + } + + /** + * Shape non-styled text. + * + * This function shapes the text of the given range under the context of given context range. + * Some script, e.g. Arabic or Devanagari, changes letter shape based on its location or + * surrounding characters. + * + * @param text a text buffer to be shaped. Any styled spans stored in this text are ignored. + * @param start a start index of shaping target in the buffer. + * @param count a length of shaping target in the buffer. + * @param contextStart a start index of context used for shaping in the buffer. + * @param contextCount a length of context used for shaping in the buffer. + * @param xOffset an additional amount of x offset of the result glyphs. + * @param yOffset an additional amount of y offset of the result glyphs. + * @param isRtl true if this text is shaped for RTL direction, false otherwise. + * @param paint a paint used for shaping text. + * @return a shape result + */ + @NonNull + public static PositionedGlyphs shapeTextRun( + @NonNull CharSequence text, int start, int count, int contextStart, int contextCount, + float xOffset, float yOffset, boolean isRtl, @NonNull Paint paint) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(paint); + if (text instanceof String) { + return new PositionedGlyphs( + nativeShapeTextRun( + (String) text, start, count, contextStart, contextCount, isRtl, + paint.getNativeInstance()), + paint, xOffset, yOffset); + } else { + char[] buf = new char[contextCount]; + TextUtils.getChars(text, contextStart, contextStart + contextCount, buf, 0); + return new PositionedGlyphs( + nativeShapeTextRun( + buf, start - contextStart, count, + 0, contextCount, isRtl, paint.getNativeInstance()), + paint, xOffset, yOffset); + } + } + + @FastNative + private static native long nativeShapeTextRun( + char[] text, int start, int count, int contextStart, int contextCount, + boolean isRtl, long nativePaint); + + @FastNative + private static native long nativeShapeTextRun( + String text, int start, int count, int contextStart, int contextCount, + boolean isRtl, long nativePaint); + +} diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 90d2537d97a8..d2f45621c7eb 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -334,6 +334,7 @@ cc_defaults { "jni/fonts/FontFamily.cpp", "jni/text/LineBreaker.cpp", "jni/text/MeasuredText.cpp", + "jni/text/TextShaper.cpp", ], header_libs: [ "android_graphics_jni_headers" ], diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index 12e2e8135278..f05c6aaa982c 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -73,6 +73,7 @@ extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env); extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env); extern int register_android_graphics_text_MeasuredText(JNIEnv* env); extern int register_android_graphics_text_LineBreaker(JNIEnv *env); +extern int register_android_graphics_text_TextShaper(JNIEnv *env); extern int register_android_util_PathParser(JNIEnv* env); extern int register_android_view_DisplayListCanvas(JNIEnv* env); @@ -137,6 +138,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_pdf_PdfRenderer), REG_JNI(register_android_graphics_text_MeasuredText), REG_JNI(register_android_graphics_text_LineBreaker), + REG_JNI(register_android_graphics_text_TextShaper), REG_JNI(register_android_util_PathParser), REG_JNI(register_android_view_RenderNode), diff --git a/libs/hwui/hwui/MinikinUtils.h b/libs/hwui/hwui/MinikinUtils.h index 6cde9c553d8c..a15803ad2dca 100644 --- a/libs/hwui/hwui/MinikinUtils.h +++ b/libs/hwui/hwui/MinikinUtils.h @@ -72,7 +72,7 @@ public: size_t start = 0; size_t nGlyphs = layout.nGlyphs(); for (size_t i = 0; i < nGlyphs; i++) { - const minikin::MinikinFont* nextFont = layout.getFont(i); + const minikin::MinikinFont* nextFont = layout.getFont(i)->typeface().get(); if (i > 0 && nextFont != curFont) { SkFont* skfont = &paint->getSkFont(); MinikinFontSkia::populateSkFont(skfont, curFont, layout.getFakery(start)); diff --git a/libs/hwui/jni/FontUtils.h b/libs/hwui/jni/FontUtils.h index b36b4e60e33a..f93a0daaf748 100644 --- a/libs/hwui/jni/FontUtils.h +++ b/libs/hwui/jni/FontUtils.h @@ -38,6 +38,14 @@ struct FontWrapper { minikin::Font font; }; +// We assume FontWrapper's address is the same as underlying Font's address. +// This assumption is used for looking up Java font object from native address. +// The Font object can be created without Java's Font object but all Java's Font objects point to +// the native FontWrapper. So when looking up Java object from minikin::Layout which gives us Font +// address, we lookup Font Java object from Font address with assumption that it is the same as +// FontWrapper address. +static_assert(offsetof(FontWrapper, font) == 0); + // Utility wrapper for java.util.List class ListHelper { public: diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp index bcdb4f5f1d13..0eb409557718 100644 --- a/libs/hwui/jni/fonts/Font.cpp +++ b/libs/hwui/jni/fonts/Font.cpp @@ -198,6 +198,59 @@ static jfloat Font_getFontMetrics(JNIEnv* env, jobject, jlong fontHandle, jlong return spacing; } +// Critical Native +static jlong Font_getFontInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font.typeface().get()); + + uint64_t result = font->font.style().weight(); + result |= font->font.style().slant() == minikin::FontStyle::Slant::ITALIC ? 0x10000 : 0x00000; + result |= ((static_cast<uint64_t>(minikinSkia->GetFontIndex())) << 32); + result |= ((static_cast<uint64_t>(minikinSkia->GetAxes().size())) << 48); + return result; +} + +// Critical Native +static jlong Font_getAxisInfo(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle, jint index) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + MinikinFontSkia* minikinSkia = static_cast<MinikinFontSkia*>(font->font.typeface().get()); + const minikin::FontVariation& var = minikinSkia->GetAxes().at(index); + uint32_t floatBinary = *reinterpret_cast<const uint32_t*>(&var.value); + return (static_cast<uint64_t>(var.axisTag) << 32) | static_cast<uint64_t>(floatBinary); +} + +/////////////////////////////////////////////////////////////////////////////// + +struct FontBufferWrapper { + FontBufferWrapper(const std::shared_ptr<minikin::MinikinFont>& font) : minikinFont(font) {} + // MinikinFont holds a shared pointer of SkTypeface which has reference to font data. + std::shared_ptr<minikin::MinikinFont> minikinFont; +}; + +static void unrefBuffer(jlong nativePtr) { + FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); + delete wrapper; +} + +// Critical Native +static jlong FontBufferHelper_refFontBuffer(CRITICAL_JNI_PARAMS_COMMA jlong fontHandle) { + FontWrapper* font = reinterpret_cast<FontWrapper*>(fontHandle); + return reinterpret_cast<jlong>(new FontBufferWrapper(font->font.typeface())); +} + +// Fast Native +static jobject FontBufferHelper_wrapByteBuffer(JNIEnv* env, jobject, jlong nativePtr) { + FontBufferWrapper* wrapper = reinterpret_cast<FontBufferWrapper*>(nativePtr); + return env->NewDirectByteBuffer( + const_cast<void*>(wrapper->minikinFont->GetFontData()), + wrapper->minikinFont->GetFontSize()); +} + +// Critical Native +static jlong FontBufferHelper_getReleaseFunc(CRITICAL_JNI_PARAMS) { + return reinterpret_cast<jlong>(unrefBuffer); +} + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gFontBuilderMethods[] = { @@ -211,13 +264,23 @@ static const JNINativeMethod gFontBuilderMethods[] = { static const JNINativeMethod gFontMethods[] = { { "nGetGlyphBounds", "(JIJLandroid/graphics/RectF;)F", (void*) Font_getGlyphBounds }, { "nGetFontMetrics", "(JJLandroid/graphics/Paint$FontMetrics;)F", (void*) Font_getFontMetrics }, + { "nGetFontInfo", "(J)J", (void*) Font_getFontInfo }, + { "nGetAxisInfo", "(JI)J", (void*) Font_getAxisInfo }, +}; + +static const JNINativeMethod gFontBufferHelperMethods[] = { + { "nRefFontBuffer", "(J)J", (void*) FontBufferHelper_refFontBuffer }, + { "nWrapByteBuffer", "(J)Ljava/nio/ByteBuffer;", (void*) FontBufferHelper_wrapByteBuffer }, + { "nGetReleaseFunc", "()J", (void*) FontBufferHelper_getReleaseFunc }, }; int register_android_graphics_fonts_Font(JNIEnv* env) { return RegisterMethodsOrDie(env, "android/graphics/fonts/Font$Builder", gFontBuilderMethods, NELEM(gFontBuilderMethods)) + RegisterMethodsOrDie(env, "android/graphics/fonts/Font", gFontMethods, - NELEM(gFontMethods)); + NELEM(gFontMethods)) + + RegisterMethodsOrDie(env, "android/graphics/fonts/NativeFontBufferHelper", + gFontBufferHelperMethods, NELEM(gFontBufferHelperMethods)); } } diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp new file mode 100644 index 000000000000..9d9e91f19851 --- /dev/null +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -0,0 +1,206 @@ +/* + * 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. + */ + +#undef LOG_TAG +#define LOG_TAG "TextShaper" + +#include "graphics_jni_helpers.h" +#include <nativehelper/ScopedStringChars.h> +#include <nativehelper/ScopedPrimitiveArray.h> +#include <set> +#include <algorithm> + +#include "SkPaint.h" +#include "SkTypeface.h" +#include <hwui/MinikinSkia.h> +#include <hwui/MinikinUtils.h> +#include <hwui/Paint.h> +#include <minikin/MinikinPaint.h> +#include <minikin/MinikinFont.h> + +namespace android { + +struct LayoutWrapper { + LayoutWrapper(minikin::Layout&& layout, float ascent, float descent) + : layout(std::move(layout)), ascent(ascent), descent(descent) {} + minikin::Layout layout; + float ascent; + float descent; +}; + +static void releaseLayout(jlong ptr) { + delete reinterpret_cast<LayoutWrapper*>(ptr); +} + +static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int count, + int contextStart, int contextCount, minikin::Bidi bidiFlags, + const Paint& paint, const Typeface* typeface) { + + minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(&paint, typeface); + + minikin::Layout layout = MinikinUtils::doLayout(&paint, bidiFlags, typeface, + text, textSize, start, count, contextStart, contextCount, nullptr); + + std::set<const minikin::Font*> seenFonts; + float overallAscent = 0; + float overallDescent = 0; + for (int i = 0; i < layout.nGlyphs(); ++i) { + const minikin::Font* font = layout.getFont(i); + if (seenFonts.find(font) != seenFonts.end()) continue; + minikin::MinikinExtent extent = {}; + font->typeface()->GetFontExtent(&extent, minikinPaint, layout.getFakery(i)); + overallAscent = std::min(overallAscent, extent.ascent); + overallDescent = std::max(overallDescent, extent.descent); + } + + std::unique_ptr<LayoutWrapper> ptr = std::make_unique<LayoutWrapper>( + std::move(layout), overallAscent, overallDescent + ); + + return reinterpret_cast<jlong>(ptr.release()); +} + +static jlong TextShaper_shapeTextRunChars(JNIEnv *env, jobject, jcharArray charArray, + jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl, + jlong paintPtr) { + ScopedCharArrayRO text(env, charArray); + Paint* paint = reinterpret_cast<Paint*>(paintPtr); + const Typeface* typeface = paint->getAndroidTypeface(); + const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; + return shapeTextRun( + text.get(), text.size(), + start, count, + contextStart, contextCount, + bidiFlags, + *paint, typeface); + +} + +static jlong TextShaper_shapeTextRunString(JNIEnv *env, jobject, jstring string, + jint start, jint count, jint contextStart, jint contextCount, jboolean isRtl, + jlong paintPtr) { + ScopedStringChars text(env, string); + Paint* paint = reinterpret_cast<Paint*>(paintPtr); + const Typeface* typeface = paint->getAndroidTypeface(); + const minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; + return shapeTextRun( + text.get(), text.size(), + start, count, + contextStart, contextCount, + bidiFlags, + *paint, typeface); +} + +// CriticalNative +static jint TextShaper_Result_getGlyphCount(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.nGlyphs(); +} + +// CriticalNative +static jfloat TextShaper_Result_getTotalAdvance(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getAdvance(); +} + +// CriticalNative +static jfloat TextShaper_Result_getAscent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->ascent; +} + +// CriticalNative +static jfloat TextShaper_Result_getDescent(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->descent; +} + +// CriticalNative +static jint TextShaper_Result_getGlyphId(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getGlyphId(i); +} + +// CriticalNative +static jfloat TextShaper_Result_getX(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getX(i); +} + +// CriticalNative +static jfloat TextShaper_Result_getY(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->layout.getY(i); +} + +// CriticalNative +static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint i) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return reinterpret_cast<jlong>(layout->layout.getFont(i)); +} + +// CriticalNative +static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) { + return reinterpret_cast<jlong>(releaseLayout); +} + +static const JNINativeMethod gMethods[] = { + // Fast Natives + {"nativeShapeTextRun", "(" + "[C" // text + "I" // start + "I" // count + "I" // contextStart + "I" // contextCount + "Z" // isRtl + "J)" // paint + "J", // LayoutPtr + (void*) TextShaper_shapeTextRunChars}, + + {"nativeShapeTextRun", "(" + "Ljava/lang/String;" // text + "I" // start + "I" // count + "I" // contextStart + "I" // contextCount + "Z" // isRtl + "J)" // paint + "J", // LayoutPtr + (void*) TextShaper_shapeTextRunString}, + +}; + +static const JNINativeMethod gResultMethods[] = { + { "nGetGlyphCount", "(J)I", (void*)TextShaper_Result_getGlyphCount }, + { "nGetTotalAdvance", "(J)F", (void*)TextShaper_Result_getTotalAdvance }, + { "nGetAscent", "(J)F", (void*)TextShaper_Result_getAscent }, + { "nGetDescent", "(J)F", (void*)TextShaper_Result_getDescent }, + { "nGetGlyphId", "(JI)I", (void*)TextShaper_Result_getGlyphId }, + { "nGetX", "(JI)F", (void*)TextShaper_Result_getX }, + { "nGetY", "(JI)F", (void*)TextShaper_Result_getY }, + { "nGetFont", "(JI)J", (void*)TextShaper_Result_getFont }, + { "nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc }, +}; + +int register_android_graphics_text_TextShaper(JNIEnv* env) { + return RegisterMethodsOrDie(env, "android/graphics/text/TextShaper", gMethods, + NELEM(gMethods)) + + RegisterMethodsOrDie(env, "android/graphics/text/PositionedGlyphs", + gResultMethods, NELEM(gResultMethods)); +} + +} + diff --git a/non-updatable-api/current.txt b/non-updatable-api/current.txt index d46f1d1606ba..0219321f6da8 100644 --- a/non-updatable-api/current.txt +++ b/non-updatable-api/current.txt @@ -16490,6 +16490,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 @@ -16550,6 +16567,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 { @@ -48127,6 +48163,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); |