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

Commit 1dd39f3b authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Make Font class being able to create from native instance

This CL refactors followings:
- Use infomation stored in native instance as the source-of-truth.
- Being able to create Font instance from native instance.
- Use ByteBuffer as a wrapper of the native byte buffer.

Bug: 179113771
Test: atest CtsTextTestCases CtsGraphicsTestCases
Test: atest android.graphics.fonts.NativeSystemFontTest
Test: atest android.graphics.fonts.SystemFontsTest
Test: atest FrameworksCoreTests:android.text
Test: atest FrameworksCoreTests:android.graphics
Change-Id: Icc1df1c76ba78d4f8800984444439fd03970e179
parent 095e4fd9
Loading
Loading
Loading
Loading
+164 −110
Original line number Diff line number Diff line
@@ -26,8 +26,7 @@ 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.text.TextUtils;
import android.util.LongSparseLongArray;
import android.util.TypedValue;

@@ -44,7 +43,6 @@ 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;
@@ -61,14 +59,9 @@ 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<>();
    private static final NativeAllocationRegistry BUFFER_REGISTRY =
            NativeAllocationRegistry.createMalloced(
                    ByteBuffer.class.getClassLoader(), nGetReleaseNativeFont());

    private static final Object SOURCE_ID_LOCK = new Object();
    @GuardedBy("SOURCE_ID_LOCK")
@@ -79,9 +72,7 @@ public final class Font {
     * A builder class for creating new Font.
     */
    public static final class Builder {
        private static final NativeAllocationRegistry sFontRegistry =
                NativeAllocationRegistry.createMalloced(Font.class.getClassLoader(),
                    nGetReleaseNativeFont());


        private @Nullable ByteBuffer mBuffer;
        private @Nullable File mFile;
@@ -484,26 +475,15 @@ public final class Font {
            final String filePath = mFile == null ? "" : mFile.getAbsolutePath();

            long ptr;
            int fontIdentifier;
            final Font font;
            if (mFont == null) {
                ptr = nBuild(builderPtr, readonlyBuffer, filePath, mLocaleList, mWeight, italic,
                        mTtcIndex);
                long fontBufferPtr = nGetFontBufferAddress(ptr);
                synchronized (SOURCE_ID_LOCK) {
                    long id = FONT_SOURCE_ID_MAP.get(fontBufferPtr, -1);
                    if (id == -1) {
                        id = FONT_SOURCE_ID_MAP.size();
                        FONT_SOURCE_ID_MAP.put(fontBufferPtr, id);
                    }
                    fontIdentifier = (int) id;
                }
                font = new Font(ptr);
            } else {
                ptr = nClone(mFont.getNativePtr(), builderPtr, mWeight, italic, mTtcIndex);
                fontIdentifier = mFont.mSourceIdentifier;
                font = new Font(ptr);
            }
            final Font font = new Font(ptr, readonlyBuffer, mFile,
                    new FontStyle(mWeight, slant), mTtcIndex, mAxes, mLocaleList, fontIdentifier);
            sFontRegistry.registerNativeAllocation(font, ptr);
            return font;
        }

@@ -525,33 +505,32 @@ public final class Font {
    }

    private final long mNativePtr;  // address of the shared ptr of minikin::Font
    private final @NonNull ByteBuffer mBuffer;
    private final @Nullable File mFile;
    private final FontStyle mFontStyle;
    private final @IntRange(from = 0) int mTtcIndex;
    private final @Nullable FontVariationAxis[] mAxes;
    private final @NonNull String mLocaleList;
    private final int mSourceIdentifier;  // An identifier of font source data.
    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private @NonNull ByteBuffer mBuffer = null;
    @GuardedBy("mLock")
    private boolean mIsFileInitialized = false;
    @GuardedBy("mLock")
    private @Nullable File mFile = null;
    @GuardedBy("mLock")
    private FontStyle mFontStyle = null;
    @GuardedBy("mLock")
    private @Nullable FontVariationAxis[] mAxes = null;
    @GuardedBy("mLock")
    private @NonNull LocaleList mLocaleList = null;
    @GuardedBy("mLock")
    private int mSourceIdentifier = -1;

    /**
     * Use Builder instead
     *
     * Caller must increment underlying minikin::Font ref count.
     *
     * @hide
     */
    private Font(long nativePtr, @NonNull ByteBuffer buffer, @Nullable File file,
            @NonNull FontStyle fontStyle, @IntRange(from = 0) int ttcIndex,
            @Nullable FontVariationAxis[] axes, @NonNull String localeList,
            int sourceIdentifier) {
        mBuffer = buffer;
        mFile = file;
        mFontStyle = fontStyle;
    public Font(long nativePtr) {
        mNativePtr = nativePtr;
        mTtcIndex = ttcIndex;
        mAxes = axes;
        mLocaleList = localeList;
        mSourceIdentifier = sourceIdentifier;

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

    /**
@@ -563,8 +542,23 @@ public final class Font {
     * @return a font buffer
     */
    public @NonNull ByteBuffer getBuffer() {
        synchronized (mLock) {
            if (mBuffer == null) {
                // Create new instance of native FontWrapper, i.e. incrementing ref count of
                // minikin Font instance for keeping buffer fo ByteBuffer reference which may live
                // longer than this object.
                long ref = nCloneFont(mNativePtr);
                ByteBuffer fromNative = nNewByteBuffer(mNativePtr);

                // Bind ByteBuffer's lifecycle with underlying font object.
                BUFFER_REGISTRY.registerNativeAllocation(fromNative, ref);

                // JNI NewDirectBuffer creates writable ByteBuffer even if it is mmaped readonly.
                mBuffer = fromNative.asReadOnlyBuffer();
            }
            return mBuffer;
        }
    }

    /**
     * Returns a file path of this font.
@@ -574,8 +568,17 @@ public final class Font {
     * @return a file path of the font
     */
    public @Nullable File getFile() {
        synchronized (mLock) {
            if (!mIsFileInitialized) {
                String path = nGetFontPath(mNativePtr);
                if (!TextUtils.isEmpty(path)) {
                    mFile = new File(path);
                }
                mIsFileInitialized = true;
            }
            return mFile;
        }
    }

    /**
     * Get a style associated with this font.
@@ -585,8 +588,17 @@ public final class Font {
     * @return a font style
     */
    public @NonNull FontStyle getStyle() {
        synchronized (mLock) {
            if (mFontStyle == null) {
                int packedStyle = nGetPackedStyle(mNativePtr);
                mFontStyle = new FontStyle(
                        FontFileUtil.unpackWeight(packedStyle),
                        FontFileUtil.unpackItalic(packedStyle)
                                ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT);
            }
            return mFontStyle;
        }
    }

    /**
     * Get a TTC index value associated with this font.
@@ -597,7 +609,7 @@ public final class Font {
     * @return a TTC index value
     */
    public @IntRange(from = 0) int getTtcIndex() {
        return mTtcIndex;
        return nGetIndex(mNativePtr);
    }

    /**
@@ -608,7 +620,23 @@ public final class Font {
     * @return font variation settings
     */
    public @Nullable FontVariationAxis[] getAxes() {
        return mAxes == null ? null : mAxes.clone();
        synchronized (mLock) {
            if (mAxes == null) {
                int axisCount = nGetAxisCount(mNativePtr);
                mAxes = new FontVariationAxis[axisCount];
                char[] charBuffer = new char[4];
                for (int i = 0; i < axisCount; ++i) {
                    long packedAxis = nGetAxisInfo(mNativePtr, 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);
                    mAxes[i] = new FontVariationAxis(new String(charBuffer), value);
                }
            }
        }
        return mAxes;
    }

    /**
@@ -618,7 +646,17 @@ public final class Font {
     * @return a locale list
     */
    public @NonNull LocaleList getLocaleList() {
        return LocaleList.forLanguageTags(mLocaleList);
        synchronized (mLock) {
            if (mLocaleList == null) {
                String langTags = nGetLocaleList(mNativePtr);
                if (TextUtils.isEmpty(langTags)) {
                    mLocaleList = LocaleList.getEmptyLocaleList();
                } else {
                    mLocaleList = LocaleList.forLanguageTags(langTags);
                }
            }
            return mLocaleList;
        }
    }

    /**
@@ -713,8 +751,21 @@ public final class Font {
     * @return an unique identifier for the font source data.
     */
    public int getSourceIdentifier() {
        synchronized (mLock) {
            if (mSourceIdentifier == -1) {
                long bufferAddress = nGetBufferAddress(mNativePtr);
                synchronized (SOURCE_ID_LOCK) {
                    long id = FONT_SOURCE_ID_MAP.get(bufferAddress, -1);
                    if (id == -1) {
                        id = FONT_SOURCE_ID_MAP.size();
                        FONT_SOURCE_ID_MAP.append(bufferAddress, id);
                    }
                    mSourceIdentifier = (int) id;
                }
            }
            return mSourceIdentifier;
        }
    }

    /**
     * Returns true if the given font is created from the same source data from this font.
@@ -736,13 +787,16 @@ public final class Font {
    private boolean isSameSource(@NonNull Font other) {
        Objects.requireNonNull(other);

        ByteBuffer myBuffer = getBuffer();
        ByteBuffer otherBuffer = other.getBuffer();

        // Shortcut for the same instance.
        if (mBuffer == other.mBuffer) {
        if (myBuffer == otherBuffer) {
            return true;
        }

        // Shortcut for different font buffer check by comparing size.
        if (mBuffer.capacity() != other.mBuffer.capacity()) {
        if (myBuffer.capacity() != otherBuffer.capacity()) {
            return false;
        }

@@ -750,15 +804,15 @@ public final class Font {
        // underlying native font object holds buffer address, check if this buffer points exactly
        // the same address as a shortcut of equality. For being compatible with of API30 or before,
        // check buffer position even if the buffer points the same address.
        if (mSourceIdentifier == other.mSourceIdentifier
                && mBuffer.position() == other.mBuffer.position()) {
        if (getSourceIdentifier() == other.getSourceIdentifier()
                && myBuffer.position() == otherBuffer.position()) {
            return true;
        }

        // Unfortunately, need to compare bytes one-by-one since the buffer may be different font
        // file but has the same file size, or two font has same content but they are allocated
        // differently. For being compatible with API30 ore before, compare with ByteBuffer#equals.
        return mBuffer.equals(other.mBuffer);
        return myBuffer.equals(otherBuffer);
    }

    @Override
@@ -769,10 +823,20 @@ public final class Font {
        if (!(o instanceof Font)) {
            return false;
        }

        Font f = (Font) o;
        boolean paramEqual = mFontStyle.equals(f.mFontStyle) && f.mTtcIndex == mTtcIndex
                && Arrays.equals(f.mAxes, mAxes) && Objects.equals(f.mLocaleList, mLocaleList)
                && Objects.equals(mFile, f.mFile);

        // The underlying minikin::Font object is the source of the truth of font information. Thus,
        // Pointer equality is the object equality.
        if (nGetMinikinFontPtr(mNativePtr) == nGetMinikinFontPtr(f.mNativePtr)) {
            return true;
        }

        boolean paramEqual = f.getStyle().equals(getStyle())
                && f.getTtcIndex() == getTtcIndex()
                && Arrays.equals(f.getAxes(), getAxes())
                && Objects.equals(f.getLocaleList(), getLocaleList())
                && Objects.equals(getFile(), f.getFile());

        if (!paramEqual) {
            return false;
@@ -784,64 +848,42 @@ public final class Font {
    @Override
    public int hashCode() {
        return Objects.hash(
                mFontStyle,
                mTtcIndex,
                Arrays.hashCode(mAxes),
                getStyle(),
                getTtcIndex(),
                Arrays.hashCode(getAxes()),
                // Use Buffer size instead of ByteBuffer#hashCode since ByteBuffer#hashCode traverse
                // data which is not performant e.g. for HashMap. The hash collision are less likely
                // happens because it is unlikely happens the different font files has exactly the
                // same size.
                mLocaleList);
                getLocaleList());
    }

    @Override
    public String toString() {
        return "Font {"
            + "path=" + mFile
            + ", style=" + mFontStyle
            + ", ttcIndex=" + mTtcIndex
            + ", axes=" + FontVariationAxis.toFontVariationSettings(mAxes)
            + ", localeList=" + mLocaleList
            + ", buffer=" + mBuffer
            + "path=" + getFile()
            + ", style=" + getStyle()
            + ", ttcIndex=" + getTtcIndex()
            + ", axes=" + FontVariationAxis.toFontVariationSettings(getAxes())
            + ", localeList=" + getLocaleList()
            + ", buffer=" + getBuffer()
            + "}";
    }

    /**
     * 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;
                }
            }
    @CriticalNative
    private static native long nGetMinikinFontPtr(long font);

            // If not found, create Font object from native object for Java API users.
            ByteBuffer buffer = NativeFontBufferHelper.refByteBuffer(ptr);
            NativeFont.Font font = NativeFont.readNativeFont(ptr);
    @CriticalNative
    private static native long nCloneFont(long font);

            Font.Builder builder = new Font.Builder(buffer, font.getFile(), "")
                    .setWeight(font.getStyle().getWeight())
                    .setSlant(font.getStyle().getSlant())
                    .setTtcIndex(font.getIndex())
                    .setFontVariationSettings(font.getAxes());
    @FastNative
    private static native ByteBuffer nNewByteBuffer(long font);

            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 nGetBufferAddress(long font);

    @CriticalNative
    private static native long nGetReleaseNativeFont();

    @FastNative
    private static native float nGetGlyphBounds(long font, int glyphId, long paint, RectF rect);
@@ -849,9 +891,21 @@ public final class Font {
    @FastNative
    private static native float nGetFontMetrics(long font, long paint, Paint.FontMetrics metrics);

    @FastNative
    private static native String nGetFontPath(long fontPtr);

    @FastNative
    private static native String nGetLocaleList(long familyPtr);

    @CriticalNative
    private static native int nGetPackedStyle(long fontPtr);

    @CriticalNative
    private static native int nGetIndex(long fontPtr);

    @CriticalNative
    private static native long nGetNativeFontPtr(long ptr);
    private static native int nGetAxisCount(long fontPtr);

    @CriticalNative
    private static native long nGetFontBufferAddress(long font);
    private static native long nGetAxisInfo(long fontPtr, int i);
}
+11 −4
Original line number Diff line number Diff line
@@ -143,12 +143,10 @@ public final class FontFamily {
        private static native long nGetReleaseNativeFamily();
    }

    private final ArrayList<Font> mFonts;
    private final long mNativePtr;

    // Use Builder instead.
    private FontFamily(@NonNull ArrayList<Font> fonts, long ptr) {
        mFonts = fonts;
        mNativePtr = ptr;
    }

@@ -176,7 +174,10 @@ public final class FontFamily {
     * @return a registered font
     */
    public @NonNull Font getFont(@IntRange(from = 0) int index) {
        return mFonts.get(index);
        if (index < 0 || getSize() <= index) {
            throw new IndexOutOfBoundsException();
        }
        return new Font(nGetFont(mNativePtr, index));
    }

    /**
@@ -185,7 +186,7 @@ public final class FontFamily {
     * @return the number of fonts registered in this family.
     */
    public @IntRange(from = 1) int getSize() {
        return mFonts.size();
        return nGetFontSize(mNativePtr);
    }

    /** @hide */
@@ -193,6 +194,12 @@ public final class FontFamily {
        return mNativePtr;
    }

    @CriticalNative
    private static native int nGetFontSize(long family);

    @CriticalNative
    private static native long nGetFont(long family, int i);

    @FastNative
    private static native String nGetLangTags(long family);

+0 −205
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.graphics.Typeface;

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

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * Read native font objects.
 *
 * @hide
 */
public class NativeFont {

    /**
     * Represents native font object.
     */
    public static final class Font {
        private final File mFile;
        private final int mIndex;
        private final FontVariationAxis[] mAxes;
        private final FontStyle mStyle;

        public Font(File file, int index, FontVariationAxis[] axes, FontStyle style) {
            mFile = file;
            mIndex = index;
            mAxes = axes;
            mStyle = style;
        }

        public File getFile() {
            return mFile;
        }

        public FontVariationAxis[] getAxes() {
            return mAxes;
        }

        public FontStyle getStyle() {
            return mStyle;
        }

        public int getIndex() {
            return mIndex;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Font font = (Font) o;
            return mIndex == font.mIndex && mFile.equals(font.mFile)
                    && Arrays.equals(mAxes, font.mAxes) && mStyle.equals(font.mStyle);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(mFile, mIndex, mStyle);
            result = 31 * result + Arrays.hashCode(mAxes);
            return result;
        }
    }

    /**
     * Represents native font family object.
     */
    public static final class Family {
        private final List<Font> mFonts;
        private final String mLocale;

        public Family(List<Font> fonts, String locale) {
            mFonts = fonts;
            mLocale = locale;
        }

        public List<Font> getFonts() {
            return mFonts;
        }

        public String getLocale() {
            return mLocale;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Family family = (Family) o;
            return mFonts.equals(family.mFonts) && mLocale.equals(family.mLocale);
        }

        @Override
        public int hashCode() {
            return Objects.hash(mFonts, mLocale);
        }
    }

    /**
     * Get underlying font families from Typeface
     *
     * @param typeface a typeface
     * @return list of family
     */
    public static List<Family> readTypeface(Typeface typeface) {
        int familyCount = nGetFamilyCount(typeface.native_instance);
        List<Family> result = new ArrayList<>(familyCount);
        for (int i = 0; i < familyCount; ++i) {
            result.add(readNativeFamily(nGetFamily(typeface.native_instance, i)));
        }
        return result;
    }

    /**
     * Read family object from native pointer
     *
     * @param familyPtr a font family pointer
     * @return a family
     */
    public static Family readNativeFamily(long familyPtr) {
        int fontCount = nGetFontCount(familyPtr);
        List<Font> result = new ArrayList<>(fontCount);
        for (int i = 0; i < fontCount; ++i) {
            result.add(readNativeFont(nGetFont(familyPtr, i)));
        }
        String localeList = nGetLocaleList(familyPtr);
        return new Family(result, localeList);
    }

    /**
     * Read font object from native pointer.
     *
     * @param ptr a font pointer
     * @return a font
     */
    public static Font readNativeFont(long 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);
        }
        String path = nGetFontPath(ptr);
        File file = (path == null) ? null : new File(path);
        FontStyle style = new FontStyle(weight,
                italic ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT);

        return new Font(file, ttcIndex, axes, style);
    }

    @CriticalNative
    private static native int nGetFamilyCount(long ptr);

    @CriticalNative
    private static native long nGetFamily(long ptr, int index);

    @FastNative
    private static native String nGetLocaleList(long familyPtr);

    @CriticalNative
    private static native long nGetFont(long familyPtr, int fontIndex);

    @CriticalNative
    private static native int nGetFontCount(long familyPtr);

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

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

    @FastNative
    private static native String nGetFontPath(long fontPtr);
}
+0 −62

File deleted.

Preview size limit exceeded, changes collapsed.

+2 −4
Original line number Diff line number Diff line
@@ -184,7 +184,7 @@ public final class PositionedGlyphs {
            long ptr = nGetFont(layoutPtr, i);
            if (prevPtr != ptr) {
                prevPtr = ptr;
                prevFont = Font.findOrCreateFontFromNativePtr(ptr);
                prevFont = new Font(ptr);
            }
            mFonts.add(prevFont);
        }
@@ -224,9 +224,7 @@ public final class PositionedGlyphs {
            if (getGlyphId(i) != that.getGlyphId(i)) return false;
            if (getGlyphX(i) != that.getGlyphX(i)) return false;
            if (getGlyphY(i) != that.getGlyphY(i)) return false;
            // Intentionally using reference equality since font equality is heavy due to buffer
            // compare.
            if (getFont(i) != that.getFont(i)) return false;
            if (!getFont(i).equals(that.getFont(i))) return false;
        }

        return true;
Loading