From 8d8d5cf2943da78ca5f84d1729b081312c031e6a Mon Sep 17 00:00:00 2001 From: Ruben Brunk Date: Fri, 28 Jun 2013 20:02:54 -0700 Subject: Added jpeg streaming classes. - Provides streaming operations for decompressing/compressing JPEG files. - Allows pixel operations to be performed on large JPEG images without holding the entire bitmap in memory. Change-Id: I597ddf282b59d2ba6d6bca4722208121e3728f94 --- .../gallery3d/jpegstream/JPEGInputStream.java | 193 +++++++++++++++++++++ .../gallery3d/jpegstream/JPEGOutputStream.java | 144 +++++++++++++++ .../android/gallery3d/jpegstream/JpegConfig.java | 32 ++++ .../android/gallery3d/jpegstream/StreamUtils.java | 80 +++++++++ 4 files changed, 449 insertions(+) create mode 100644 gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java create mode 100644 gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java create mode 100644 gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java create mode 100644 gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java (limited to 'gallerycommon/src/com/android') diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java new file mode 100644 index 000000000..44ccd4c6b --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2013 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 com.android.gallery3d.jpegstream; + +import android.graphics.Point; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class JPEGInputStream extends FilterInputStream { + private long JNIPointer = 0; // Used by JNI code. Don't touch. + + private boolean mValidConfig = false; + private boolean mConfigChanged = false; + private int mFormat = -1; + private byte[] mTmpBuffer = new byte[1]; + private int mWidth = 0; + private int mHeight = 0; + + public JPEGInputStream(InputStream in) { + super(in); + } + + public JPEGInputStream(InputStream in, int format) { + super(in); + setConfig(format); + } + + public boolean setConfig(int format) { + // Make sure format is valid + switch (format) { + case JpegConfig.FORMAT_GRAYSCALE: + case JpegConfig.FORMAT_RGB: + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + break; + default: + return false; + } + mFormat = format; + mValidConfig = true; + mConfigChanged = true; + return true; + } + + public Point getDimensions() throws IOException { + if (mValidConfig) { + applyConfigChange(); + return new Point(mWidth, mHeight); + } + return null; + } + + @Override + public int available() { + return 0; // TODO + } + + @Override + public void close() throws IOException { + cleanup(); + super.close(); + } + + @Override + public synchronized void mark(int readlimit) { + // Do nothing + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int read() throws IOException { + read(mTmpBuffer, 0, 1); + return 0xFF & mTmpBuffer[0]; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + if (offset < 0 || count < 0 || (offset + count) > buffer.length) { + throw new ArrayIndexOutOfBoundsException(String.format( + " buffer length %d, offset %d, length %d", + buffer.length, offset, count)); + } + if (!mValidConfig) { + return 0; + } + applyConfigChange(); + int flag = JpegConfig.J_ERROR_FATAL; + try { + flag = readDecodedBytes(buffer, offset, count); + } finally { + if (flag < 0) { + cleanup(); + } + } + if (flag < 0) { + switch (flag) { + case JpegConfig.J_DONE: + return -1; // Returns -1 after reading EOS. + default: + throw new IOException("Error reading jpeg stream"); + } + } + return flag; + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("Reset not supported."); + } + + @Override + public long skip(long byteCount) throws IOException { + if (byteCount <= 0) { + return 0; + } + // Shorten skip to a reasonable amount + int flag = skipDecodedBytes((int) (0x7FFFFFFF & byteCount)); + if (flag < 0) { + switch (flag) { + case JpegConfig.J_DONE: + return 0; // Returns 0 after reading EOS. + default: + throw new IOException("Error skipping jpeg stream"); + } + } + return flag; + } + + @Override + protected void finalize() throws Throwable { + try { + cleanup(); + } finally { + super.finalize(); + } + } + + private void applyConfigChange() throws IOException { + if (mConfigChanged) { + cleanup(); + Point dimens = new Point(0, 0); + int flag = setup(dimens, in, mFormat); + switch(flag) { + case JpegConfig.J_SUCCESS: + break; // allow setup to continue + case JpegConfig.J_ERROR_BAD_ARGS: + throw new IllegalArgumentException("Bad arguments to read"); + default: + throw new IOException("Error to reading jpeg headers."); + } + mWidth = dimens.x; + mHeight = dimens.y; + mConfigChanged = false; + } + } + + native private int setup(Point dimens, InputStream in, int format); + + native private void cleanup(); + + native private int readDecodedBytes( byte[] inBuffer, int offset, int inCount); + + native private int skipDecodedBytes(int bytes); + + static { + System.loadLibrary("jni_jpegstream"); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java new file mode 100644 index 000000000..c49d3759c --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2013 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 com.android.gallery3d.jpegstream; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +public class JPEGOutputStream extends FilterOutputStream { + private long JNIPointer = 0; // Used by JNI code. Don't touch. + + private byte[] mTmpBuffer = new byte[1]; + private int mWidth = 0; + private int mHeight = 0; + private int mQuality = 0; + private int mFormat = -1; + private boolean mValidConfig = false; + private boolean mConfigChanged = false; + + public JPEGOutputStream(OutputStream out) { + super(out); + } + + public JPEGOutputStream(OutputStream out, int width, int height, int quality, + int format) { + super(out); + setConfig(width, height, quality, format); + } + + public boolean setConfig(int width, int height, int quality, int format) { + // Clamp quality to range (0, 100] + quality = Math.max(Math.min(quality, 100), 1); + + // Make sure format is valid + switch (format) { + case JpegConfig.FORMAT_GRAYSCALE: + case JpegConfig.FORMAT_RGB: + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + break; + default: + return false; + } + + // If valid, set configuration + if (width > 0 && height > 0) { + mWidth = width; + mHeight = height; + mFormat = format; + mQuality = quality; + mValidConfig = true; + mConfigChanged = true; + } else { + return false; + } + + return mValidConfig; + } + + @Override + public void close() throws IOException { + cleanup(); + super.close(); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + if (offset < 0 || length < 0 || (offset + length) > buffer.length) { + throw new ArrayIndexOutOfBoundsException(String.format( + " buffer length %d, offset %d, length %d", + buffer.length, offset, length)); + } + if (!mValidConfig) { + return; + } + if (mConfigChanged) { + cleanup(); + int flag = setup(out, mWidth, mHeight, mFormat, mQuality); + switch(flag) { + case JpegConfig.J_SUCCESS: + break; // allow setup to continue + case JpegConfig.J_ERROR_BAD_ARGS: + throw new IllegalArgumentException("Bad arguments to write"); + default: + throw new IOException("Error to writing jpeg headers."); + } + mConfigChanged = false; + } + int returnCode = JpegConfig.J_ERROR_FATAL; + try { + returnCode = writeInputBytes(buffer, offset, length); + } finally { + if (returnCode < 0) { + cleanup(); + } + } + if (returnCode < 0) { + throw new IOException("Error writing jpeg stream"); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public void write(int oneByte) throws IOException { + mTmpBuffer[0] = (byte) oneByte; + write(mTmpBuffer); + } + + @Override + protected void finalize() throws Throwable { + try { + cleanup(); + } finally { + super.finalize(); + } + } + + native private int setup(OutputStream out, int width, int height, int format, int quality); + + native private void cleanup(); + + native private int writeInputBytes(byte[] inBuffer, int offset, int inCount); + + static { + System.loadLibrary("jni_jpegstream"); + } +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java new file mode 100644 index 000000000..e514e3b8d --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 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 com.android.gallery3d.jpegstream; + +public interface JpegConfig { + // Pixel formats + public static final int FORMAT_GRAYSCALE = 0x001; // 1 byte/pixel + public static final int FORMAT_RGB = 0x003; // 3 bytes/pixel RGBRGBRGBRGB... + public static final int FORMAT_RGBA = 0x004; // 4 bytes/pixel RGBARGBARGBARGBA... + public static final int FORMAT_ABGR = 0x104; // 4 bytes/pixel ABGRABGRABGR... + + // Jni error codes + static final int J_SUCCESS = 0; + static final int J_ERROR_FATAL = -1; + static final int J_ERROR_BAD_ARGS = -2; + static final int J_EXCEPTION = -3; + static final int J_DONE = -4; +} diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java new file mode 100644 index 000000000..abd8f681e --- /dev/null +++ b/gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2013 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 com.android.gallery3d.jpegstream; + +import java.nio.ByteOrder; + +public class StreamUtils { + + private StreamUtils() { + } + + /** + * Copies the input byte array into the output int array with the given + * endianness. If input is not a multiple of 4, ignores the last 1-3 bytes + * and returns true. + */ + public static boolean byteToIntArray(int[] output, byte[] input, ByteOrder endianness) { + int length = input.length - (input.length % 4); + if (output.length * 4 < length) { + throw new ArrayIndexOutOfBoundsException("Output array is too short to hold input"); + } + if (endianness == ByteOrder.BIG_ENDIAN) { + for (int i = 0, j = 0; i < output.length; i++, j += 4) { + output[i] = ((input[j] & 0xFF) << 24) | ((input[j + 1] & 0xFF) << 16) + | ((input[j + 2] & 0xFF) << 8) | ((input[j + 3] & 0xFF)); + } + } else { + for (int i = 0, j = 0; i < output.length; i++, j += 4) { + output[i] = ((input[j + 3] & 0xFF) << 24) | ((input[j + 2] & 0xFF) << 16) + | ((input[j + 1] & 0xFF) << 8) | ((input[j] & 0xFF)); + } + } + return input.length % 4 != 0; + } + + public static int[] byteToIntArray(byte[] input, ByteOrder endianness) { + int[] output = new int[input.length / 4]; + byteToIntArray(output, input, endianness); + return output; + } + + /** + * Uses native endianness. + */ + public static int[] byteToIntArray(byte[] input) { + return byteToIntArray(input, ByteOrder.nativeOrder()); + } + + /** + * Returns the number of bytes in a pixel for a given format defined in + * JpegConfig. + */ + public static int pixelSize(int format) { + switch (format) { + case JpegConfig.FORMAT_ABGR: + case JpegConfig.FORMAT_RGBA: + return 4; + case JpegConfig.FORMAT_RGB: + return 3; + case JpegConfig.FORMAT_GRAYSCALE: + return 1; + default: + return -1; + } + } +} -- cgit v1.2.3