| /* |
| * Copyright 2021 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 androidx.emoji2.text; |
| |
| import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; |
| |
| import android.content.res.AssetManager; |
| |
| import androidx.annotation.AnyThread; |
| import androidx.annotation.IntRange; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.RequiresApi; |
| import androidx.annotation.RestrictTo; |
| import androidx.text.emoji.flatbuffer.MetadataList; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| |
| /** |
| * Reads the emoji metadata from a given InputStream or ByteBuffer. |
| * |
| * @hide |
| */ |
| @RestrictTo(LIBRARY_GROUP_PREFIX) |
| @AnyThread |
| @RequiresApi(19) |
| class MetadataListReader { |
| |
| /** |
| * Meta tag for emoji metadata. This string is used by the font update script to insert the |
| * emoji meta into the font. This meta table contains the list of all emojis which are stored in |
| * binary format using FlatBuffers. This flat list is later converted by the system into a trie. |
| * {@code int} representation for "Emji" |
| * |
| * @see MetadataRepo |
| */ |
| private static final int EMJI_TAG = 'E' << 24 | 'm' << 16 | 'j' << 8 | 'i'; |
| |
| /** |
| * Deprecated meta tag name. Do not use, kept for compatibility reasons, will be removed soon. |
| */ |
| private static final int EMJI_TAG_DEPRECATED = 'e' << 24 | 'm' << 16 | 'j' << 8 | 'i'; |
| |
| /** |
| * The name of the meta table in the font. int representation for "meta" |
| */ |
| private static final int META_TABLE_NAME = 'm' << 24 | 'e' << 16 | 't' << 8 | 'a'; |
| |
| /** |
| * Construct MetadataList from an input stream. Does not close the given InputStream, therefore |
| * it is caller's responsibility to properly close the stream. |
| * |
| * @param inputStream InputStream to read emoji metadata from |
| */ |
| static MetadataList read(InputStream inputStream) throws IOException { |
| final OpenTypeReader openTypeReader = new InputStreamOpenTypeReader(inputStream); |
| final OffsetInfo offsetInfo = findOffsetInfo(openTypeReader); |
| // skip to where metadata is |
| openTypeReader.skip((int) (offsetInfo.getStartOffset() - openTypeReader.getPosition())); |
| // allocate a ByteBuffer and read into it since FlatBuffers can read only from a ByteBuffer |
| final ByteBuffer buffer = ByteBuffer.allocate((int) offsetInfo.getLength()); |
| final int numRead = inputStream.read(buffer.array()); |
| if (numRead != offsetInfo.getLength()) { |
| throw new IOException("Needed " + offsetInfo.getLength() + " bytes, got " + numRead); |
| } |
| |
| return MetadataList.getRootAsMetadataList(buffer); |
| } |
| |
| /** |
| * Construct MetadataList from a byte buffer. |
| * |
| * @param byteBuffer ByteBuffer to read emoji metadata from |
| */ |
| static MetadataList read(final ByteBuffer byteBuffer) throws IOException { |
| final ByteBuffer newBuffer = byteBuffer.duplicate(); |
| final OpenTypeReader reader = new ByteBufferReader(newBuffer); |
| final OffsetInfo offsetInfo = findOffsetInfo(reader); |
| // skip to where metadata is |
| newBuffer.position((int) offsetInfo.getStartOffset()); |
| return MetadataList.getRootAsMetadataList(newBuffer); |
| } |
| |
| /** |
| * Construct MetadataList from an asset. |
| * |
| * @param assetManager AssetManager instance |
| * @param assetPath asset manager path of the file that the Typeface and metadata will be |
| * created from |
| */ |
| static MetadataList read(AssetManager assetManager, String assetPath) |
| throws IOException { |
| try (InputStream inputStream = assetManager.open(assetPath)) { |
| return read(inputStream); |
| } |
| } |
| |
| /** |
| * Finds the start offset and length of the emoji metadata in the font. |
| * |
| * @return OffsetInfo which contains start offset and length of the emoji metadata in the font |
| * |
| * @throws IOException |
| */ |
| private static OffsetInfo findOffsetInfo(OpenTypeReader reader) throws IOException { |
| // skip sfnt version |
| reader.skip(OpenTypeReader.UINT32_BYTE_COUNT); |
| // start of Table Count |
| final int tableCount = reader.readUnsignedShort(); |
| if (tableCount > 100) { |
| //something is wrong quit |
| throw new IOException("Cannot read metadata."); |
| } |
| //skip to beginning of tables data |
| reader.skip(OpenTypeReader.UINT16_BYTE_COUNT * 3); |
| |
| long metaOffset = -1; |
| for (int i = 0; i < tableCount; i++) { |
| final int tag = reader.readTag(); |
| // skip checksum |
| reader.skip(OpenTypeReader.UINT32_BYTE_COUNT); |
| final long offset = reader.readUnsignedInt(); |
| // skip mLength |
| reader.skip(OpenTypeReader.UINT32_BYTE_COUNT); |
| if (META_TABLE_NAME == tag) { |
| metaOffset = offset; |
| break; |
| } |
| } |
| |
| if (metaOffset != -1) { |
| // skip to the beginning of meta tables. |
| reader.skip((int) (metaOffset - reader.getPosition())); |
| // skip minorVersion, majorVersion, flags, reserved, |
| reader.skip( |
| OpenTypeReader.UINT16_BYTE_COUNT * 2 + OpenTypeReader.UINT32_BYTE_COUNT * 2); |
| final long mapsCount = reader.readUnsignedInt(); |
| for (int i = 0; i < mapsCount; i++) { |
| final int tag = reader.readTag(); |
| final long dataOffset = reader.readUnsignedInt(); |
| final long dataLength = reader.readUnsignedInt(); |
| if (EMJI_TAG == tag || EMJI_TAG_DEPRECATED == tag) { |
| return new OffsetInfo(dataOffset + metaOffset, dataLength); |
| } |
| } |
| } |
| |
| throw new IOException("Cannot read metadata."); |
| } |
| |
| /** |
| * Start offset and length of the emoji metadata in the font. |
| */ |
| private static class OffsetInfo { |
| private final long mStartOffset; |
| private final long mLength; |
| |
| OffsetInfo(long startOffset, long length) { |
| mStartOffset = startOffset; |
| mLength = length; |
| } |
| |
| long getStartOffset() { |
| return mStartOffset; |
| } |
| |
| long getLength() { |
| return mLength; |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static int toUnsignedShort(final short value) { |
| return value & 0xFFFF; |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| static long toUnsignedInt(final int value) { |
| return value & 0xFFFFFFFFL; |
| } |
| |
| private interface OpenTypeReader { |
| int UINT16_BYTE_COUNT = 2; |
| int UINT32_BYTE_COUNT = 4; |
| |
| /** |
| * Reads an {@code OpenType uint16}. |
| * |
| * @throws IOException |
| */ |
| int readUnsignedShort() throws IOException; |
| |
| /** |
| * Reads an {@code OpenType uint32}. |
| * |
| * @throws IOException |
| */ |
| long readUnsignedInt() throws IOException; |
| |
| /** |
| * Reads an {@code OpenType Tag}. |
| * |
| * @throws IOException |
| */ |
| int readTag() throws IOException; |
| |
| /** |
| * Skip the given amount of numOfBytes |
| * |
| * @throws IOException |
| */ |
| void skip(int numOfBytes) throws IOException; |
| |
| /** |
| * @return the position of the reader |
| */ |
| long getPosition(); |
| } |
| |
| /** |
| * Reads {@code OpenType} data from an {@link InputStream}. |
| */ |
| private static class InputStreamOpenTypeReader implements OpenTypeReader { |
| |
| private final @NonNull byte[] mByteArray; |
| private final @NonNull ByteBuffer mByteBuffer; |
| private final @NonNull InputStream mInputStream; |
| private long mPosition = 0; |
| |
| /** |
| * Constructs the reader with the given InputStream. Does not close the InputStream, it is |
| * caller's responsibility to close it. |
| * |
| * @param inputStream InputStream to read from |
| */ |
| InputStreamOpenTypeReader(@NonNull final InputStream inputStream) { |
| mInputStream = inputStream; |
| mByteArray = new byte[UINT32_BYTE_COUNT]; |
| mByteBuffer = ByteBuffer.wrap(mByteArray); |
| mByteBuffer.order(ByteOrder.BIG_ENDIAN); |
| } |
| |
| @Override |
| public int readUnsignedShort() throws IOException { |
| mByteBuffer.position(0); |
| read(UINT16_BYTE_COUNT); |
| return toUnsignedShort(mByteBuffer.getShort()); |
| } |
| |
| @Override |
| public long readUnsignedInt() throws IOException { |
| mByteBuffer.position(0); |
| read(UINT32_BYTE_COUNT); |
| return toUnsignedInt(mByteBuffer.getInt()); |
| } |
| |
| @Override |
| public int readTag() throws IOException { |
| mByteBuffer.position(0); |
| read(UINT32_BYTE_COUNT); |
| return mByteBuffer.getInt(); |
| } |
| |
| @Override |
| public void skip(int numOfBytes) throws IOException { |
| while (numOfBytes > 0) { |
| int skipped = (int) mInputStream.skip(numOfBytes); |
| if (skipped < 1) { |
| throw new IOException("Skip didn't move at least 1 byte forward"); |
| } |
| numOfBytes -= skipped; |
| mPosition += skipped; |
| } |
| } |
| |
| @Override |
| public long getPosition() { |
| return mPosition; |
| } |
| |
| private void read(@IntRange(from = 0, to = UINT32_BYTE_COUNT) final int numOfBytes) |
| throws IOException { |
| if (mInputStream.read(mByteArray, 0, numOfBytes) != numOfBytes) { |
| throw new IOException("read failed"); |
| } |
| mPosition += numOfBytes; |
| } |
| } |
| |
| /** |
| * Reads OpenType data from a ByteBuffer. |
| */ |
| private static class ByteBufferReader implements OpenTypeReader { |
| |
| private final @NonNull ByteBuffer mByteBuffer; |
| |
| /** |
| * Constructs the reader with the given ByteBuffer. |
| * |
| * @param byteBuffer ByteBuffer to read from |
| */ |
| ByteBufferReader(@NonNull final ByteBuffer byteBuffer) { |
| mByteBuffer = byteBuffer; |
| mByteBuffer.order(ByteOrder.BIG_ENDIAN); |
| } |
| |
| @Override |
| public int readUnsignedShort() throws IOException { |
| return toUnsignedShort(mByteBuffer.getShort()); |
| } |
| |
| @Override |
| public long readUnsignedInt() throws IOException { |
| return toUnsignedInt(mByteBuffer.getInt()); |
| } |
| |
| @Override |
| public int readTag() throws IOException { |
| return mByteBuffer.getInt(); |
| } |
| |
| @Override |
| public void skip(final int numOfBytes) throws IOException { |
| mByteBuffer.position(mByteBuffer.position() + numOfBytes); |
| } |
| |
| @Override |
| public long getPosition() { |
| return mByteBuffer.position(); |
| } |
| } |
| |
| private MetadataListReader() { |
| } |
| } |