1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package androidx.emoji.text;
17
18import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19
20import android.content.res.AssetManager;
21
22import androidx.annotation.AnyThread;
23import androidx.annotation.IntRange;
24import androidx.annotation.RequiresApi;
25import androidx.annotation.RestrictTo;
26import androidx.text.emoji.flatbuffer.MetadataList;
27
28import java.io.IOException;
29import java.io.InputStream;
30import java.nio.ByteBuffer;
31import java.nio.ByteOrder;
32
33/**
34 * Reads the emoji metadata from a given InputStream or ByteBuffer.
35 *
36 * @hide
37 */
38@RestrictTo(LIBRARY_GROUP)
39@AnyThread
40@RequiresApi(19)
41class MetadataListReader {
42
43    /**
44     * Meta tag for emoji metadata. This string is used by the font update script to insert the
45     * emoji meta into the font. This meta table contains the list of all emojis which are stored in
46     * binary format using FlatBuffers. This flat list is later converted by the system into a trie.
47     * {@code int} representation for "Emji"
48     *
49     * @see MetadataRepo
50     */
51    private static final int EMJI_TAG = 'E' << 24 | 'm' << 16 | 'j' << 8 | 'i';
52
53    /**
54     * Deprecated meta tag name. Do not use, kept for compatibility reasons, will be removed soon.
55     */
56    private static final int EMJI_TAG_DEPRECATED = 'e' << 24 | 'm' << 16 | 'j' << 8 | 'i';
57
58    /**
59     * The name of the meta table in the font. int representation for "meta"
60     */
61    private static final int META_TABLE_NAME = 'm' << 24 | 'e' << 16 | 't' << 8 | 'a';
62
63    /**
64     * Construct MetadataList from an input stream. Does not close the given InputStream, therefore
65     * it is caller's responsibility to properly close the stream.
66     *
67     * @param inputStream InputStream to read emoji metadata from
68     */
69    static MetadataList read(InputStream inputStream) throws IOException {
70        final OpenTypeReader openTypeReader = new InputStreamOpenTypeReader(inputStream);
71        final OffsetInfo offsetInfo = findOffsetInfo(openTypeReader);
72        // skip to where metadata is
73        openTypeReader.skip((int) (offsetInfo.getStartOffset() - openTypeReader.getPosition()));
74        // allocate a ByteBuffer and read into it since FlatBuffers can read only from a ByteBuffer
75        final ByteBuffer buffer = ByteBuffer.allocate((int) offsetInfo.getLength());
76        final int numRead = inputStream.read(buffer.array());
77        if (numRead != offsetInfo.getLength()) {
78            throw new IOException("Needed " + offsetInfo.getLength() + " bytes, got " + numRead);
79        }
80
81        return MetadataList.getRootAsMetadataList(buffer);
82    }
83
84    /**
85     * Construct MetadataList from a byte buffer.
86     *
87     * @param byteBuffer ByteBuffer to read emoji metadata from
88     */
89    static MetadataList read(final ByteBuffer byteBuffer) throws IOException {
90        final ByteBuffer newBuffer = byteBuffer.duplicate();
91        final OpenTypeReader reader = new ByteBufferReader(newBuffer);
92        final OffsetInfo offsetInfo = findOffsetInfo(reader);
93        // skip to where metadata is
94        newBuffer.position((int) offsetInfo.getStartOffset());
95        return MetadataList.getRootAsMetadataList(newBuffer);
96    }
97
98    /**
99     * Construct MetadataList from an asset.
100     *
101     * @param assetManager AssetManager instance
102     * @param assetPath asset manager path of the file that the Typeface and metadata will be
103     *                  created from
104     */
105    static MetadataList read(AssetManager assetManager, String assetPath)
106            throws IOException {
107        try (InputStream inputStream = assetManager.open(assetPath)) {
108            return read(inputStream);
109        }
110    }
111
112    /**
113     * Finds the start offset and length of the emoji metadata in the font.
114     *
115     * @return OffsetInfo which contains start offset and length of the emoji metadata in the font
116     *
117     * @throws IOException
118     */
119    private static OffsetInfo findOffsetInfo(OpenTypeReader reader) throws IOException {
120        // skip sfnt version
121        reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
122        // start of Table Count
123        final int tableCount = reader.readUnsignedShort();
124        if (tableCount > 100) {
125            //something is wrong quit
126            throw new IOException("Cannot read metadata.");
127        }
128        //skip to begining of tables data
129        reader.skip(OpenTypeReader.UINT16_BYTE_COUNT * 3);
130
131        long metaOffset = -1;
132        for (int i = 0; i < tableCount; i++) {
133            final int tag = reader.readTag();
134            // skip checksum
135            reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
136            final long offset = reader.readUnsignedInt();
137            // skip mLength
138            reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
139            if (META_TABLE_NAME == tag) {
140                metaOffset = offset;
141                break;
142            }
143        }
144
145        if (metaOffset != -1) {
146            // skip to the begining of meta tables.
147            reader.skip((int) (metaOffset - reader.getPosition()));
148            // skip minorVersion, majorVersion, flags, reserved,
149            reader.skip(
150                    OpenTypeReader.UINT16_BYTE_COUNT * 2 + OpenTypeReader.UINT32_BYTE_COUNT * 2);
151            final long mapsCount = reader.readUnsignedInt();
152            for (int i = 0; i < mapsCount; i++) {
153                final int tag = reader.readTag();
154                final long dataOffset = reader.readUnsignedInt();
155                final long dataLength = reader.readUnsignedInt();
156                if (EMJI_TAG == tag || EMJI_TAG_DEPRECATED == tag) {
157                    return new OffsetInfo(dataOffset + metaOffset, dataLength);
158                }
159            }
160        }
161
162        throw new IOException("Cannot read metadata.");
163    }
164
165    /**
166     * Start offset and length of the emoji metadata in the font.
167     */
168    private static class OffsetInfo {
169        private final long mStartOffset;
170        private final long mLength;
171
172        OffsetInfo(long startOffset, long length) {
173            mStartOffset = startOffset;
174            mLength = length;
175        }
176
177        long getStartOffset() {
178            return mStartOffset;
179        }
180
181        long getLength() {
182            return mLength;
183        }
184    }
185
186    private static int toUnsignedShort(final short value) {
187        return value & 0xFFFF;
188    }
189
190    private static long toUnsignedInt(final int value) {
191        return value & 0xFFFFFFFFL;
192    }
193
194    private interface OpenTypeReader {
195        int UINT16_BYTE_COUNT = 2;
196        int UINT32_BYTE_COUNT = 4;
197
198        /**
199         * Reads an {@code OpenType uint16}.
200         *
201         * @throws IOException
202         */
203        int readUnsignedShort() throws IOException;
204
205        /**
206         * Reads an {@code OpenType uint32}.
207         *
208         * @throws IOException
209         */
210        long readUnsignedInt() throws IOException;
211
212        /**
213         * Reads an {@code OpenType Tag}.
214         *
215         * @throws IOException
216         */
217        int readTag() throws IOException;
218
219        /**
220         * Skip the given amount of numOfBytes
221         *
222         * @throws IOException
223         */
224        void skip(int numOfBytes) throws IOException;
225
226        /**
227         * @return the position of the reader
228         */
229        long getPosition();
230    }
231
232    /**
233     * Reads {@code OpenType} data from an {@link InputStream}.
234     */
235    private static class InputStreamOpenTypeReader implements OpenTypeReader {
236
237        private final byte[] mByteArray;
238        private final ByteBuffer mByteBuffer;
239        private final InputStream mInputStream;
240        private long mPosition = 0;
241
242        /**
243         * Constructs the reader with the given InputStream. Does not close the InputStream, it is
244         * caller's responsibility to close it.
245         *
246         * @param inputStream InputStream to read from
247         */
248        InputStreamOpenTypeReader(final InputStream inputStream) {
249            mInputStream = inputStream;
250            mByteArray = new byte[UINT32_BYTE_COUNT];
251            mByteBuffer = ByteBuffer.wrap(mByteArray);
252            mByteBuffer.order(ByteOrder.BIG_ENDIAN);
253        }
254
255        @Override
256        public int readUnsignedShort() throws IOException {
257            mByteBuffer.position(0);
258            read(UINT16_BYTE_COUNT);
259            return toUnsignedShort(mByteBuffer.getShort());
260        }
261
262        @Override
263        public long readUnsignedInt() throws IOException {
264            mByteBuffer.position(0);
265            read(UINT32_BYTE_COUNT);
266            return toUnsignedInt(mByteBuffer.getInt());
267        }
268
269        @Override
270        public int readTag() throws IOException {
271            mByteBuffer.position(0);
272            read(UINT32_BYTE_COUNT);
273            return mByteBuffer.getInt();
274        }
275
276        @Override
277        public void skip(int numOfBytes) throws IOException {
278            while (numOfBytes > 0) {
279                int skipped = (int) mInputStream.skip(numOfBytes);
280                if (skipped < 1) {
281                    throw new IOException("Skip didn't move at least 1 byte forward");
282                }
283                numOfBytes -= skipped;
284                mPosition += skipped;
285            }
286        }
287
288        @Override
289        public long getPosition() {
290            return mPosition;
291        }
292
293        private void read(@IntRange(from = 0, to = UINT32_BYTE_COUNT) final int numOfBytes)
294                throws IOException {
295            if (mInputStream.read(mByteArray, 0, numOfBytes) != numOfBytes) {
296                throw new IOException("read failed");
297            }
298            mPosition += numOfBytes;
299        }
300    }
301
302    /**
303     * Reads OpenType data from a ByteBuffer.
304     */
305    private static class ByteBufferReader implements OpenTypeReader {
306
307        private final ByteBuffer mByteBuffer;
308
309        /**
310         * Constructs the reader with the given ByteBuffer.
311         *
312         * @param byteBuffer ByteBuffer to read from
313         */
314        ByteBufferReader(final ByteBuffer byteBuffer) {
315            mByteBuffer = byteBuffer;
316            mByteBuffer.order(ByteOrder.BIG_ENDIAN);
317        }
318
319        @Override
320        public int readUnsignedShort() throws IOException {
321            return toUnsignedShort(mByteBuffer.getShort());
322        }
323
324        @Override
325        public long readUnsignedInt() throws IOException {
326            return toUnsignedInt(mByteBuffer.getInt());
327        }
328
329        @Override
330        public int readTag() throws IOException {
331            return mByteBuffer.getInt();
332        }
333
334        @Override
335        public void skip(final int numOfBytes) throws IOException {
336            mByteBuffer.position(mByteBuffer.position() + numOfBytes);
337        }
338
339        @Override
340        public long getPosition() {
341            return mByteBuffer.position();
342        }
343    }
344
345    private MetadataListReader() {
346    }
347}
348