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 
17 package android.opengl;
18 
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.nio.Buffer;
23 import java.nio.ByteBuffer;
24 import java.nio.ByteOrder;
25 
26 /**
27  * Utility methods for using ETC1 compressed textures.
28  *
29  */
30 public 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      */
loadTexture(int target, int level, int border, int fallbackFormat, int fallbackType, InputStream input)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      */
loadTexture(int target, int level, int border, int fallbackFormat, int fallbackType, ETC1Texture texture)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      */
isETC1Supported()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 {
ETC1Texture(int width, int height, ByteBuffer data)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          */
getWidth()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          */
getHeight()133         public int getHeight() { return mHeight; }
134 
135         /**
136          * Get the compressed data of the texture.
137          * @return the texture data.
138          */
getData()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      */
createTexture(InputStream input)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      */
compressTexture(Buffer input, int width, int height, int pixelSize, int stride)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      */
writeTexture(ETC1Texture texture, OutputStream output)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