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