1/*
2 * Copyright (C) 2009 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 */
16
17package android.opengl;
18
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.OutputStream;
22import java.nio.Buffer;
23import java.nio.ByteBuffer;
24import java.nio.ByteOrder;
25
26/**
27 * Utility methods for using ETC1 compressed textures.
28 *
29 */
30public class ETC1Util {
31    /**
32     * Convenience method to load an ETC1 texture whether or not the active OpenGL context
33     * supports the ETC1 texture compression format.
34     * @param target the texture target.
35     * @param level the texture level
36     * @param border the border size. Typically 0.
37     * @param fallbackFormat the format to use if ETC1 texture compression is not supported.
38     * Must be GL_RGB.
39     * @param fallbackType the type to use if ETC1 texture compression is not supported.
40     * Can be either GL_UNSIGNED_SHORT_5_6_5, which implies 16-bits-per-pixel,
41     * or GL_UNSIGNED_BYTE, which implies 24-bits-per-pixel.
42     * @param input the input stream containing an ETC1 texture in PKM format.
43     * @throws IOException
44     */
45    public static void loadTexture(int target, int level, int border,
46            int fallbackFormat, int fallbackType, InputStream input)
47        throws IOException {
48        loadTexture(target, level, border, fallbackFormat, fallbackType, createTexture(input));
49    }
50
51    /**
52     * Convenience method to load an ETC1 texture whether or not the active OpenGL context
53     * supports the ETC1 texture compression format.
54     * @param target the texture target.
55     * @param level the texture level
56     * @param border the border size. Typically 0.
57     * @param fallbackFormat the format to use if ETC1 texture compression is not supported.
58     * Must be GL_RGB.
59     * @param fallbackType the type to use if ETC1 texture compression is not supported.
60     * Can be either GL_UNSIGNED_SHORT_5_6_5, which implies 16-bits-per-pixel,
61     * or GL_UNSIGNED_BYTE, which implies 24-bits-per-pixel.
62     * @param texture the ETC1 to load.
63     */
64    public static void loadTexture(int target, int level, int border,
65            int fallbackFormat, int fallbackType, ETC1Texture texture) {
66        if (fallbackFormat != GLES10.GL_RGB) {
67            throw new IllegalArgumentException("fallbackFormat must be GL_RGB");
68        }
69        if (! (fallbackType == GLES10.GL_UNSIGNED_SHORT_5_6_5
70                || fallbackType == GLES10.GL_UNSIGNED_BYTE)) {
71            throw new IllegalArgumentException("Unsupported fallbackType");
72        }
73
74        int width = texture.getWidth();
75        int height = texture.getHeight();
76        Buffer data = texture.getData();
77        if (isETC1Supported()) {
78            int imageSize = data.remaining();
79            GLES10.glCompressedTexImage2D(target, level, ETC1.ETC1_RGB8_OES, width, height,
80                    border, imageSize, data);
81        } else {
82            boolean useShorts = fallbackType != GLES10.GL_UNSIGNED_BYTE;
83            int pixelSize = useShorts ? 2 : 3;
84            int stride = pixelSize * width;
85            ByteBuffer decodedData = ByteBuffer.allocateDirect(stride*height)
86                .order(ByteOrder.nativeOrder());
87            ETC1.decodeImage(data, decodedData, width, height, pixelSize, stride);
88            GLES10.glTexImage2D(target, level, fallbackFormat, width, height, border,
89                    fallbackFormat, fallbackType, decodedData);
90        }
91    }
92
93    /**
94     * Check if ETC1 texture compression is supported by the active OpenGL ES context.
95     * @return true if the active OpenGL ES context supports ETC1 texture compression.
96     */
97    public static boolean isETC1Supported() {
98        int[] results = new int[20];
99        GLES10.glGetIntegerv(GLES10.GL_NUM_COMPRESSED_TEXTURE_FORMATS, results, 0);
100        int numFormats = results[0];
101        if (numFormats > results.length) {
102            results = new int[numFormats];
103        }
104        GLES10.glGetIntegerv(GLES10.GL_COMPRESSED_TEXTURE_FORMATS, results, 0);
105        for (int i = 0; i < numFormats; i++) {
106            if (results[i] == ETC1.ETC1_RGB8_OES) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113    /**
114     * A utility class encapsulating a compressed ETC1 texture.
115     */
116    public static class ETC1Texture {
117        public ETC1Texture(int width, int height, ByteBuffer data) {
118            mWidth = width;
119            mHeight = height;
120            mData = data;
121        }
122
123        /**
124         * Get the width of the texture in pixels.
125         * @return the width of the texture in pixels.
126         */
127        public int getWidth() { return mWidth; }
128
129        /**
130         * Get the height of the texture in pixels.
131         * @return the width of the texture in pixels.
132         */
133        public int getHeight() { return mHeight; }
134
135        /**
136         * Get the compressed data of the texture.
137         * @return the texture data.
138         */
139        public ByteBuffer getData() { return mData; }
140
141        private int mWidth;
142        private int mHeight;
143        private ByteBuffer mData;
144    }
145
146    /**
147     * Create a new ETC1Texture from an input stream containing a PKM formatted compressed texture.
148     * @param input an input stream containing a PKM formatted compressed texture.
149     * @return an ETC1Texture read from the input stream.
150     * @throws IOException
151     */
152    public static ETC1Texture createTexture(InputStream input) throws IOException {
153        int width = 0;
154        int height = 0;
155        byte[] ioBuffer = new byte[4096];
156        {
157            if (input.read(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE) != ETC1.ETC_PKM_HEADER_SIZE) {
158                throw new IOException("Unable to read PKM file header.");
159            }
160            ByteBuffer headerBuffer = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE)
161                .order(ByteOrder.nativeOrder());
162            headerBuffer.put(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE).position(0);
163            if (!ETC1.isValid(headerBuffer)) {
164                throw new IOException("Not a PKM file.");
165            }
166            width = ETC1.getWidth(headerBuffer);
167            height = ETC1.getHeight(headerBuffer);
168        }
169        int encodedSize = ETC1.getEncodedDataSize(width, height);
170        ByteBuffer dataBuffer = ByteBuffer.allocateDirect(encodedSize).order(ByteOrder.nativeOrder());
171        for (int i = 0; i < encodedSize; ) {
172            int chunkSize = Math.min(ioBuffer.length, encodedSize - i);
173            if (input.read(ioBuffer, 0, chunkSize) != chunkSize) {
174                throw new IOException("Unable to read PKM file data.");
175            }
176            dataBuffer.put(ioBuffer, 0, chunkSize);
177            i += chunkSize;
178        }
179        dataBuffer.position(0);
180        return new ETC1Texture(width, height, dataBuffer);
181    }
182
183    /**
184     * Helper function that compresses an image into an ETC1Texture.
185     * @param input a native order direct buffer containing the image data
186     * @param width the width of the image in pixels
187     * @param height the height of the image in pixels
188     * @param pixelSize the size of a pixel in bytes (2 or 3)
189     * @param stride the width of a line of the image in bytes
190     * @return the ETC1 texture.
191     */
192    public static ETC1Texture compressTexture(Buffer input, int width, int height, int pixelSize, int stride){
193        int encodedImageSize = ETC1.getEncodedDataSize(width, height);
194        ByteBuffer compressedImage = ByteBuffer.allocateDirect(encodedImageSize).
195            order(ByteOrder.nativeOrder());
196        ETC1.encodeImage(input, width, height, pixelSize, stride, compressedImage);
197        return new ETC1Texture(width, height, compressedImage);
198    }
199
200    /**
201     * Helper function that writes an ETC1Texture to an output stream formatted as a PKM file.
202     * @param texture the input texture.
203     * @param output the stream to write the formatted texture data to.
204     * @throws IOException
205     */
206    public static void writeTexture(ETC1Texture texture, OutputStream output) throws IOException {
207        ByteBuffer dataBuffer = texture.getData();
208        int originalPosition = dataBuffer.position();
209        try {
210            int width = texture.getWidth();
211            int height = texture.getHeight();
212            ByteBuffer header = ByteBuffer.allocateDirect(ETC1.ETC_PKM_HEADER_SIZE).order(ByteOrder.nativeOrder());
213            ETC1.formatHeader(header, width, height);
214            byte[] ioBuffer = new byte[4096];
215            header.get(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE);
216            output.write(ioBuffer, 0, ETC1.ETC_PKM_HEADER_SIZE);
217            int encodedSize = ETC1.getEncodedDataSize(width, height);
218            for (int i = 0; i < encodedSize; ) {
219                int chunkSize = Math.min(ioBuffer.length, encodedSize - i);
220                dataBuffer.get(ioBuffer, 0, chunkSize);
221                output.write(ioBuffer, 0, chunkSize);
222                i += chunkSize;
223            }
224        } finally {
225            dataBuffer.position(originalPosition);
226        }
227    }
228}
229