diff options
author | Ruben Brunk <rubenbrunk@google.com> | 2013-06-28 20:02:54 -0700 |
---|---|---|
committer | Ruben Brunk <rubenbrunk@google.com> | 2013-07-11 13:41:29 -0700 |
commit | 8d8d5cf2943da78ca5f84d1729b081312c031e6a (patch) | |
tree | f4c60b00a469733ff11901bd8fdc030c8f537f5c | |
parent | 3ea8558300780ca5269d75230f78eadf4100876c (diff) | |
download | android_packages_apps_Snap-8d8d5cf2943da78ca5f84d1729b081312c031e6a.tar.gz android_packages_apps_Snap-8d8d5cf2943da78ca5f84d1729b081312c031e6a.tar.bz2 android_packages_apps_Snap-8d8d5cf2943da78ca5f84d1729b081312c031e6a.zip |
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
26 files changed, 2624 insertions, 0 deletions
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; + } + } +} diff --git a/jni_jpegstream/src/error_codes.h b/jni_jpegstream/src/error_codes.h new file mode 100644 index 000000000..be55a0099 --- /dev/null +++ b/jni_jpegstream/src/error_codes.h @@ -0,0 +1,26 @@ +/* + * 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. + */ + +#ifndef JPEG_ERROR_CODES_H_ +#define JPEG_ERROR_CODES_H_ + +#define J_DONE -4 +#define J_EXCEPTION -3 +#define J_ERROR_BAD_ARGS -2 +#define J_ERROR_FATAL -1 +#define J_SUCCESS 0 + +#endif // JPEG_ERROR_CODES_H_ diff --git a/jni_jpegstream/src/inputstream_wrapper.cpp b/jni_jpegstream/src/inputstream_wrapper.cpp new file mode 100644 index 000000000..98721b066 --- /dev/null +++ b/jni_jpegstream/src/inputstream_wrapper.cpp @@ -0,0 +1,69 @@ +/* + * 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. + */ + +#include "inputstream_wrapper.h" +#include "error_codes.h" + +jmethodID InputStreamWrapper::sReadID = NULL; +jmethodID InputStreamWrapper::sSkipID = NULL; + +int32_t InputStreamWrapper::read(int32_t length, int32_t offset) { + if (offset < 0 || length < 0 || (offset + length) > getBufferSize()) { + return J_ERROR_BAD_ARGS; + } + int32_t bytesRead = 0; + mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_COMMIT); + mBytes = NULL; + if (mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + bytesRead = static_cast<int32_t>(mEnv->CallIntMethod(mStream, sReadID, + mByteArray, offset, length)); + if (mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + mBytes = mEnv->GetByteArrayElements(mByteArray, NULL); + if (mBytes == NULL || mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + if (bytesRead == END_OF_STREAM) { + return J_DONE; + } + return bytesRead; +} + +int64_t InputStreamWrapper::skip(int64_t count) { + int64_t bytesSkipped = 0; + bytesSkipped = static_cast<int64_t>(mEnv->CallLongMethod(mStream, sSkipID, + static_cast<jlong>(count))); + if (mEnv->ExceptionCheck() || bytesSkipped < 0) { + return J_EXCEPTION; + } + return bytesSkipped; +} + +// Acts like a read call that returns the End Of Image marker for a JPEG file. +int32_t InputStreamWrapper::forceReadEOI() { + mBytes[0] = (jbyte) 0xFF; + mBytes[1] = (jbyte) 0xD9; + return 2; +} + +void InputStreamWrapper::setReadSkipMethodIDs(jmethodID readID, + jmethodID skipID) { + sReadID = readID; + sSkipID = skipID; +} diff --git a/jni_jpegstream/src/inputstream_wrapper.h b/jni_jpegstream/src/inputstream_wrapper.h new file mode 100644 index 000000000..ed9942bab --- /dev/null +++ b/jni_jpegstream/src/inputstream_wrapper.h @@ -0,0 +1,38 @@ +/* + * 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. + */ + +#ifndef INPUTSTREAM_WRAPPER_H_ +#define INPUTSTREAM_WRAPPER_H_ + +#include "jni_defines.h" +#include "stream_wrapper.h" + +#include <stdint.h> + +class InputStreamWrapper : public StreamWrapper { +public: + virtual int32_t read(int32_t length, int32_t offset); + virtual int64_t skip(int64_t count); + virtual int32_t forceReadEOI(); + + // Call this in JNI_OnLoad to cache read/skip method IDs + static void setReadSkipMethodIDs(jmethodID readID, jmethodID skipID); +protected: + static jmethodID sReadID; + static jmethodID sSkipID; +}; + +#endif // INPUTSTREAM_WRAPPER_H_ diff --git a/jni_jpegstream/src/jerr_hook.cpp b/jni_jpegstream/src/jerr_hook.cpp new file mode 100644 index 000000000..f8f864f78 --- /dev/null +++ b/jni_jpegstream/src/jerr_hook.cpp @@ -0,0 +1,52 @@ +/* + * 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. + */ + + +#include "jerr_hook.h" +#include "jni_defines.h" + +/** + * Replaces libjpeg's error_exit function, returns control to + * the point + */ +void ErrExit(j_common_ptr cinfo) { + ErrManager* mgr = reinterpret_cast<ErrManager*>(cinfo->err); + (*cinfo->err->output_message) (cinfo); + // Returns control to error handling in jpeg_writer + longjmp(mgr->setjmp_buf, 1); +} + +/** + * Replaces libjpeg's output_message function, writes message + * to logcat's error log. + */ +void ErrOutput(j_common_ptr cinfo) { + ErrManager* mgr = reinterpret_cast<ErrManager*>(cinfo->err); + char buf[JMSG_LENGTH_MAX]; + (*cinfo->err->format_message) (cinfo, buf); + buf[JMSG_LENGTH_MAX - 1] = '\0'; // Force null terminator + // Output error message in ndk logcat. + LOGE("%s\n", buf); +} + +void SetupErrMgr(j_common_ptr cinfo, ErrManager* errMgr) { + jpeg_std_error(&(errMgr->mgr)); + errMgr->mgr.error_exit = ErrExit; + errMgr->mgr.output_message = ErrOutput; + cinfo->err = reinterpret_cast<struct jpeg_error_mgr*>(errMgr); +} + + diff --git a/jni_jpegstream/src/jerr_hook.h b/jni_jpegstream/src/jerr_hook.h new file mode 100644 index 000000000..f2ba7cd03 --- /dev/null +++ b/jni_jpegstream/src/jerr_hook.h @@ -0,0 +1,43 @@ +/* + * 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. + */ + +#ifndef JERR_HOOK_H_ +#define JERR_HOOK_H_ + +extern "C" { +#include "jinclude.h" +#include "jpeglib.h" +#include "jerror.h" +} + +#include <setjmp.h> + +/** + * ErrManager replaces libjpeg's default error handling with + * the following behavior: + * - libjpeg function calls return to the position set by + * setjmp for error cleanup. + * - libjpeg error and warning messages are printed to + * logcat's error output. + */ +typedef struct { + struct jpeg_error_mgr mgr; + jmp_buf setjmp_buf; +} ErrManager; + +void SetupErrMgr(j_common_ptr cinfo, ErrManager* errMgr); + +#endif // JERR_HOOK_H_ diff --git a/jni_jpegstream/src/jni_defines.h b/jni_jpegstream/src/jni_defines.h new file mode 100644 index 000000000..8c9bd0430 --- /dev/null +++ b/jni_jpegstream/src/jni_defines.h @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef JNIDEFINES_H +#define JNIDEFINES_H + + +#include <jni.h> +#include <string.h> +#include <android/log.h> + +#define LOGV(msg...) __android_log_print(ANDROID_LOG_VERBOSE, "Native_JPEGStream", msg) +#define LOGE(msg...) __android_log_print(ANDROID_LOG_ERROR, "Native_JPEGStream", msg) +#define LOGW(msg...) __android_log_print(ANDROID_LOG_WARN, "Native_JPEGStream", msg) + +#endif // JNIDEFINES_H diff --git a/jni_jpegstream/src/jpeg_config.h b/jni_jpegstream/src/jpeg_config.h new file mode 100644 index 000000000..a99755275 --- /dev/null +++ b/jni_jpegstream/src/jpeg_config.h @@ -0,0 +1,31 @@ +/* + * 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. + */ + +#ifndef JPEG_CONFIG_H_ +#define JPEG_CONFIG_H_ +namespace Jpeg_Config { + +// Pixel format +enum Format { + FORMAT_GRAYSCALE = 0x001, // 1 byte/pixel + FORMAT_RGB = 0x003, // 3 bytes/pixel RGBRGBRGBRGB... + FORMAT_RGBA = 0x004, // 4 bytes/pixel RGBARGBARGBARGBA... + FORMAT_ABGR = 0x104 // 4 bytes/pixel ABGRABGRABGR... +}; + +} // end namespace Jpeg_Config + +#endif // JPEG_CONFIG_H_ diff --git a/jni_jpegstream/src/jpeg_hook.cpp b/jni_jpegstream/src/jpeg_hook.cpp new file mode 100644 index 000000000..cca54e405 --- /dev/null +++ b/jni_jpegstream/src/jpeg_hook.cpp @@ -0,0 +1,198 @@ +/* + * 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. + */ + +#include "error_codes.h" +#include "jni_defines.h" +#include "jpeg_hook.h" + +#include <stddef.h> +#include <string.h> + +void Mgr_init_destination_fcn(j_compress_ptr cinfo) { + DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest); + dst->mgr.next_output_byte = reinterpret_cast<JOCTET*>(dst->outStream->getBufferPtr()); + dst->mgr.free_in_buffer = dst->outStream->getBufferSize(); +} + +boolean Mgr_empty_output_buffer_fcn(j_compress_ptr cinfo) { + DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest); + int32_t len = dst->outStream->getBufferSize(); + if (dst->outStream->write(len, 0) != J_SUCCESS) { + ERREXIT(cinfo, JERR_FILE_WRITE); + } + dst->mgr.next_output_byte = reinterpret_cast<JOCTET*>(dst->outStream->getBufferPtr()); + dst->mgr.free_in_buffer = len; + return TRUE; +} + +void Mgr_term_destination_fcn(j_compress_ptr cinfo) { + DestManager *dst = reinterpret_cast<DestManager*>(cinfo->dest); + int32_t remaining = dst->outStream->getBufferSize() - dst->mgr.free_in_buffer; + if (dst->outStream->write(remaining, 0) != J_SUCCESS) { + ERREXIT(cinfo, JERR_FILE_WRITE); + } +} + +int32_t MakeDst(j_compress_ptr cinfo, JNIEnv *env, jobject outStream) { + if (cinfo->dest != NULL) { + LOGE("DestManager already exists, cannot allocate!"); + return J_ERROR_FATAL; + } else { + size_t size = sizeof(DestManager); + cinfo->dest = (struct jpeg_destination_mgr *) (*cinfo->mem->alloc_small) + ((j_common_ptr) cinfo, JPOOL_PERMANENT, size); + if (cinfo->dest == NULL) { + LOGE("Could not allocate memory for DestManager."); + return J_ERROR_FATAL; + } + memset(cinfo->dest, '0', size); + } + DestManager *d = reinterpret_cast<DestManager*>(cinfo->dest); + d->mgr.init_destination = Mgr_init_destination_fcn; + d->mgr.empty_output_buffer = Mgr_empty_output_buffer_fcn; + d->mgr.term_destination = Mgr_term_destination_fcn; + d->outStream = new OutputStreamWrapper(); + if(d->outStream->init(env, outStream)) { + return J_SUCCESS; + } + return J_ERROR_FATAL; +} + +void UpdateDstEnv(j_compress_ptr cinfo, JNIEnv* env) { + DestManager* d = reinterpret_cast<DestManager*>(cinfo->dest); + d->outStream->updateEnv(env); +} + +void CleanDst(j_compress_ptr cinfo) { + if (cinfo != NULL && cinfo->dest != NULL) { + DestManager *d = reinterpret_cast<DestManager*>(cinfo->dest); + if (d->outStream != NULL) { + delete d->outStream; + d->outStream = NULL; + } + } +} + +boolean Mgr_fill_input_buffer_fcn(j_decompress_ptr cinfo) { + SourceManager *src = reinterpret_cast<SourceManager*>(cinfo->src); + int32_t bytesRead = src->inStream->read(src->inStream->getBufferSize(), 0); + if (bytesRead == J_DONE) { + if (src->start_of_file == TRUE) { + ERREXIT(cinfo, JERR_INPUT_EMPTY); + } + WARNMS(cinfo, JWRN_JPEG_EOF); + bytesRead = src->inStream->forceReadEOI(); + } else if (bytesRead < 0) { + ERREXIT(cinfo, JERR_FILE_READ); + } else if (bytesRead == 0) { + LOGW("read 0 bytes from InputStream."); + } + src->mgr.next_input_byte = reinterpret_cast<JOCTET*>(src->inStream->getBufferPtr()); + src->mgr.bytes_in_buffer = bytesRead; + if (bytesRead != 0) { + src->start_of_file = FALSE; + } + return TRUE; +} + +void Mgr_init_source_fcn(j_decompress_ptr cinfo) { + SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src); + s->start_of_file = TRUE; + Mgr_fill_input_buffer_fcn(cinfo); +} + +void Mgr_skip_input_data_fcn(j_decompress_ptr cinfo, long num_bytes) { + // Cannot skip negative or 0 bytes. + if (num_bytes <= 0) { + LOGW("skipping 0 bytes in InputStream"); + return; + } + SourceManager *src = reinterpret_cast<SourceManager*>(cinfo->src); + if (src->mgr.bytes_in_buffer >= num_bytes) { + src->mgr.bytes_in_buffer -= num_bytes; + src->mgr.next_input_byte += num_bytes; + } else { + // if skipping more bytes than remain in buffer, set skip_bytes + int64_t skip = num_bytes - src->mgr.bytes_in_buffer; + src->mgr.next_input_byte += src->mgr.bytes_in_buffer; + src->mgr.bytes_in_buffer = 0; + int64_t actual = src->inStream->skip(skip); + if (actual < 0) { + ERREXIT(cinfo, JERR_FILE_READ); + } + skip -= actual; + while (skip > 0) { + actual = src->inStream->skip(skip); + if (actual < 0) { + ERREXIT(cinfo, JERR_FILE_READ); + } + skip -= actual; + if (actual == 0) { + // Multiple zero byte skips, likely EOF + WARNMS(cinfo, JWRN_JPEG_EOF); + return; + } + } + } +} + +void Mgr_term_source_fcn(j_decompress_ptr cinfo) { + //noop +} + +int32_t MakeSrc(j_decompress_ptr cinfo, JNIEnv *env, jobject inStream){ + if (cinfo->src != NULL) { + LOGE("SourceManager already exists, cannot allocate!"); + return J_ERROR_FATAL; + } else { + size_t size = sizeof(SourceManager); + cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small) + ((j_common_ptr) cinfo, JPOOL_PERMANENT, size); + if (cinfo->src == NULL) { + // Could not allocate memory. + LOGE("Could not allocate memory for SourceManager."); + return J_ERROR_FATAL; + } + memset(cinfo->src, '0', size); + } + SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src); + s->start_of_file = TRUE; + s->mgr.init_source = Mgr_init_source_fcn; + s->mgr.fill_input_buffer = Mgr_fill_input_buffer_fcn; + s->mgr.skip_input_data = Mgr_skip_input_data_fcn; + s->mgr.resync_to_restart = jpeg_resync_to_restart; // use default restart + s->mgr.term_source = Mgr_term_source_fcn; + s->inStream = new InputStreamWrapper(); + if(s->inStream->init(env, inStream)) { + return J_SUCCESS; + } + return J_ERROR_FATAL; +} + +void UpdateSrcEnv(j_decompress_ptr cinfo, JNIEnv* env) { + SourceManager* s = reinterpret_cast<SourceManager*>(cinfo->src); + s->inStream->updateEnv(env); +} + +void CleanSrc(j_decompress_ptr cinfo) { + if (cinfo != NULL && cinfo->src != NULL) { + SourceManager *s = reinterpret_cast<SourceManager*>(cinfo->src); + if (s->inStream != NULL) { + delete s->inStream; + s->inStream = NULL; + } + } +} diff --git a/jni_jpegstream/src/jpeg_hook.h b/jni_jpegstream/src/jpeg_hook.h new file mode 100644 index 000000000..b02bb34c0 --- /dev/null +++ b/jni_jpegstream/src/jpeg_hook.h @@ -0,0 +1,76 @@ +/* + * 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. + */ + +#ifndef LIBJPEG_HOOK_H_ +#define LIBJPEG_HOOK_H_ + +extern "C" { +#include "jinclude.h" +#include "jpeglib.h" +#include "jerror.h" +} + +#include "inputstream_wrapper.h" +#include "outputstream_wrapper.h" + +#include <stdint.h> + +/** + * DestManager holds the libjpeg destination manager struct and + * a holder with a java OutputStream. + */ +typedef struct { + struct jpeg_destination_mgr mgr; + OutputStreamWrapper *outStream; +} DestManager; + +// Initializes the DestManager struct, sets up the jni refs +int32_t MakeDst(j_compress_ptr cinfo, JNIEnv *env, jobject outStream); + +/** + * Updates the jni env pointer. This should be called in the beginning of any + * JNI method in jpegstream.cpp before CleanDst or any of the libjpeg functions + * that can trigger a call to an OutputStreamWrapper method. + */ +void UpdateDstEnv(j_compress_ptr cinfo, JNIEnv* env); + +// Cleans the jni refs. To wipe the compress object call jpeg_destroy_compress +void CleanDst(j_compress_ptr cinfo); + +/** + * SourceManager holds the libjpeg source manager struct and a + * holder with a java InputStream. + */ +typedef struct { + struct jpeg_source_mgr mgr; + boolean start_of_file; + InputStreamWrapper *inStream; +} SourceManager; + +// Initializes the SourceManager struct, sets up the jni refs +int32_t MakeSrc(j_decompress_ptr cinfo, JNIEnv *env, jobject inStream); + +/** + * Updates the jni env pointer. This should be called in the beginning of any + * JNI method in jpegstream.cpp before CleanSrc or any of the libjpeg functions + * that can trigger a call to an InputStreamWrapper method. + */ +void UpdateSrcEnv(j_decompress_ptr cinfo, JNIEnv* env); + +// Cleans the jni refs. To wipe the decompress object, call jpeg_destroy_decompress +void CleanSrc(j_decompress_ptr cinfo); + +#endif // LIBJPEG_HOOK_H_ diff --git a/jni_jpegstream/src/jpeg_reader.cpp b/jni_jpegstream/src/jpeg_reader.cpp new file mode 100644 index 000000000..4726b6426 --- /dev/null +++ b/jni_jpegstream/src/jpeg_reader.cpp @@ -0,0 +1,254 @@ +/* + * 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. + */ + +#include "jpeg_reader.h" +#include "error_codes.h" +#include "jpeg_hook.h" + +#include <setjmp.h> + +JpegReader::JpegReader() : + mInfo(), + mErrorManager(), + mScanlineBuf(NULL), + mScanlineIter(NULL), + mScanlineBuflen(0), + mScanlineUnformattedBuflen(0), + mScanlineBytesRemaining(0), + mFormat(), + mFinished(false), + mSetup(false) {} + +JpegReader::~JpegReader() { + if (reset() != J_SUCCESS) { + LOGE("Failed to destroy compress object, JpegReader may leak memory."); + } +} + +int32_t JpegReader::setup(JNIEnv *env, jobject in, int32_t* width, int32_t* height, + Jpeg_Config::Format format) { + if (mFinished || mSetup) { + return J_ERROR_FATAL; + } + if (env->ExceptionCheck()) { + return J_EXCEPTION; + } + + // Setup error handler + SetupErrMgr(reinterpret_cast<j_common_ptr>(&mInfo), &mErrorManager); + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + + // Call libjpeg setup + jpeg_create_decompress(&mInfo); + + // Setup our data source object, this allocates java global references + int32_t flags = MakeSrc(&mInfo, env, in); + if (flags != J_SUCCESS) { + LOGE("Failed to make source with error code: %d ", flags); + return flags; + } + + // Reads jpeg file header + jpeg_read_header(&mInfo, TRUE); + jpeg_calc_output_dimensions(&mInfo); + + const int components = (static_cast<int>(format) & 0xff); + + // Do setup for input format + switch (components) { + case 1: + mInfo.out_color_space = JCS_GRAYSCALE; + mScanlineUnformattedBuflen = mInfo.output_width; + break; + case 3: + case 4: + mScanlineUnformattedBuflen = mInfo.output_width * components; + if (mInfo.jpeg_color_space == JCS_CMYK + || mInfo.jpeg_color_space == JCS_YCCK) { + // Always use cmyk for output in a 4 channel jpeg. + // libjpeg has a builtin cmyk->rgb decoder. + mScanlineUnformattedBuflen = mInfo.output_width * 4; + mInfo.out_color_space = JCS_CMYK; + } else { + mInfo.out_color_space = JCS_RGB; + } + break; + default: + return J_ERROR_BAD_ARGS; + } + + mScanlineBuflen = mInfo.output_width * components; + mScanlineBytesRemaining = mScanlineBuflen; + mScanlineBuf = (JSAMPLE *) (mInfo.mem->alloc_small)( + reinterpret_cast<j_common_ptr>(&mInfo), JPOOL_PERMANENT, + mScanlineUnformattedBuflen * sizeof(JSAMPLE)); + mScanlineIter = mScanlineBuf; + jpeg_start_decompress(&mInfo); + + // Output image dimensions + if (width != NULL) { + *width = mInfo.output_width; + } + if (height != NULL) { + *height = mInfo.output_height; + } + + mFormat = format; + mSetup = true; + return J_SUCCESS; +} + +int32_t JpegReader::read(int8_t* bytes, int32_t offset, int32_t count) { + if (!mSetup) { + return J_ERROR_FATAL; + } + if (mFinished) { + return J_DONE; + } + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + if (count <= 0) { + return J_ERROR_BAD_ARGS; + } + int32_t total_length = count; + while (mInfo.output_scanline < mInfo.output_height) { + if (count < mScanlineBytesRemaining) { + // read partial scanline and return + if (bytes != NULL) { + // Treat NULL bytes as a skip + memcpy((void*) (bytes + offset), (void*) mScanlineIter, + count * sizeof(int8_t)); + } + mScanlineBytesRemaining -= count; + mScanlineIter += count; + return total_length; + } else if (count > 0) { + // read full scanline + if (bytes != NULL) { + // Treat NULL bytes as a skip + memcpy((void*) (bytes + offset), (void*) mScanlineIter, + mScanlineBytesRemaining * sizeof(int8_t)); + bytes += mScanlineBytesRemaining; + } + count -= mScanlineBytesRemaining; + mScanlineBytesRemaining = 0; + } + // Scanline buffer exhausted, read next scanline + if (jpeg_read_scanlines(&mInfo, &mScanlineBuf, 1) != 1) { + // Always read full scanline, no IO suspension + return J_ERROR_FATAL; + } + // Do in-place pixel formatting + formatPixels(static_cast<uint8_t*>(mScanlineBuf), + mScanlineUnformattedBuflen); + + // Reset iterators + mScanlineIter = mScanlineBuf; + mScanlineBytesRemaining = mScanlineBuflen; + } + + // Read all of the scanlines + jpeg_finish_decompress(&mInfo); + mFinished = true; + return total_length - count; +} + +void JpegReader::updateEnv(JNIEnv *env) { + UpdateSrcEnv(&mInfo, env); +} + +// Does in-place pixel formatting +void JpegReader::formatPixels(uint8_t* buf, int32_t len) { + uint8_t *iter = buf; + + // Do cmyk->rgb conversion if necessary + switch (mInfo.out_color_space) { + case JCS_CMYK: + // Convert CMYK to RGB + int r, g, b, c, m, y, k; + for (int i = 0; i < len; i += 4) { + c = buf[i + 0]; + m = buf[i + 1]; + y = buf[i + 2]; + k = buf[i + 3]; + // Handle fmt for weird photoshop markers + if (mInfo.saw_Adobe_marker) { + r = (k * c) / 255; + g = (k * m) / 255; + b = (k * y) / 255; + } else { + r = (255 - k) * (255 - c) / 255; + g = (255 - k) * (255 - m) / 255; + b = (255 - k) * (255 - y) / 255; + } + *iter++ = r; + *iter++ = g; + *iter++ = b; + } + break; + case JCS_RGB: + iter += (len * 3 / 4); + break; + case JCS_GRAYSCALE: + default: + return; + } + + // Do endianness and alpha for output format + if (mFormat == Jpeg_Config::FORMAT_RGBA) { + // Set alphas to 255 + uint8_t* end = buf + len - 1; + for (int i = len - 1; i >= 0; i -= 4) { + buf[i] = 255; + buf[i - 1] = *--iter; + buf[i - 2] = *--iter; + buf[i - 3] = *--iter; + } + } else if (mFormat == Jpeg_Config::FORMAT_ABGR) { + // Reverse endianness and set alphas to 255 + uint8_t* end = buf + len - 1; + int r, g, b; + for (int i = len - 1; i >= 0; i -= 4) { + b = *--iter; + g = *--iter; + r = *--iter; + buf[i] = r; + buf[i - 1] = g; + buf[i - 2] = b; + buf[i - 3] = 255; + } + } +} + +int32_t JpegReader::reset() { + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + // Clean up global java references + CleanSrc(&mInfo); + // Wipe decompress struct, free memory pools + jpeg_destroy_decompress(&mInfo); + mFinished = false; + mSetup = false; + return J_SUCCESS; +} + diff --git a/jni_jpegstream/src/jpeg_reader.h b/jni_jpegstream/src/jpeg_reader.h new file mode 100644 index 000000000..afde27b93 --- /dev/null +++ b/jni_jpegstream/src/jpeg_reader.h @@ -0,0 +1,88 @@ +/* + * 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. + */ +#ifndef JPEG_READER_H_ +#define JPEG_READER_H_ + +#include "jerr_hook.h" +#include "jni_defines.h" +#include "jpeg_config.h" + +#include <stdint.h> + +/** + * JpegReader wraps libjpeg's decompression functionality and a + * java InputStream object. Read calls return data from the + * InputStream that has been decompressed. + */ +class JpegReader { +public: + JpegReader(); + ~JpegReader(); + + /** + * Call setup with a valid InputStream reference and pixel format. + * If this method is successful, the contents of width and height will + * be set to the dimensions of the bitmap to be read. + * + * ***This method will result in the jpeg file header being read + * from the InputStream*** + * + * Returns J_SUCCESS on success or a negative error code. + */ + int32_t setup(JNIEnv *env, jobject in, int32_t* width, int32_t* height, + Jpeg_Config::Format format); + + /** + * Decompresses bytes from the InputStream and writes at most count + * bytes into the buffer, bytes, starting at some offset. Passing a + * NULL as the bytes pointer effectively skips those bytes. + * + * ***This method will result in bytes being read from the InputStream*** + * + * Returns the number of bytes written into the input buffer or a + * negative error code. + */ + int32_t read(int8_t * bytes, int32_t offset, int32_t count); + + /** + * Updates the environment pointer. Call this before read or reset + * in any jni function call. + */ + void updateEnv(JNIEnv *env); + + /** + * Frees any java global references held by the JpegReader, destroys + * the decompress structure, and frees allocations in libjpeg's pools. + */ + int32_t reset(); + +private: + void formatPixels(uint8_t* buf, int32_t len); + struct jpeg_decompress_struct mInfo; + ErrManager mErrorManager; + + JSAMPLE* mScanlineBuf; + JSAMPLE* mScanlineIter; + int32_t mScanlineBuflen; + int32_t mScanlineUnformattedBuflen; + int32_t mScanlineBytesRemaining; + + Jpeg_Config::Format mFormat; + bool mFinished; + bool mSetup; +}; + +#endif // JPEG_READER_H_ diff --git a/jni_jpegstream/src/jpeg_writer.cpp b/jni_jpegstream/src/jpeg_writer.cpp new file mode 100644 index 000000000..4f7891778 --- /dev/null +++ b/jni_jpegstream/src/jpeg_writer.cpp @@ -0,0 +1,218 @@ +/* + * 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. + */ + +#include "jpeg_hook.h" +#include "jpeg_writer.h" +#include "error_codes.h" + +#include <setjmp.h> +#include <assert.h> + +JpegWriter::JpegWriter() : mInfo(), + mErrorManager(), + mScanlineBuf(NULL), + mScanlineIter(NULL), + mScanlineBuflen(0), + mScanlineBytesRemaining(0), + mFormat(), + mFinished(false), + mSetup(false) {} + +JpegWriter::~JpegWriter() { + if (reset() != J_SUCCESS) { + LOGE("Failed to destroy compress object, may leak memory."); + } +} + +const int32_t JpegWriter::DEFAULT_X_DENSITY = 300; +const int32_t JpegWriter::DEFAULT_Y_DENSITY = 300; +const int32_t JpegWriter::DEFAULT_DENSITY_UNIT = 1; + +int32_t JpegWriter::setup(JNIEnv *env, jobject out, int32_t width, int32_t height, + Jpeg_Config::Format format, int32_t quality) { + if (mFinished || mSetup) { + return J_ERROR_FATAL; + } + if (env->ExceptionCheck()) { + return J_EXCEPTION; + } + if (height <= 0 || width <= 0 || quality <= 0 || quality > 100) { + return J_ERROR_BAD_ARGS; + } + // Setup error handler + SetupErrMgr(reinterpret_cast<j_common_ptr>(&mInfo), &mErrorManager); + + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + + // Setup cinfo struct + jpeg_create_compress(&mInfo); + + // Setup global java refs + int32_t flags = MakeDst(&mInfo, env, out); + if (flags != J_SUCCESS) { + return flags; + } + + // Initialize width, height, and color space + mInfo.image_width = width; + mInfo.image_height = height; + const int components = (static_cast<int>(format) & 0xff); + switch (components) { + case 1: + mInfo.input_components = 1; + mInfo.in_color_space = JCS_GRAYSCALE; + break; + case 3: + case 4: + mInfo.input_components = 3; + mInfo.in_color_space = JCS_RGB; + break; + default: + return J_ERROR_BAD_ARGS; + } + + // Set defaults + jpeg_set_defaults(&mInfo); + mInfo.density_unit = DEFAULT_DENSITY_UNIT; // JFIF code for pixel size units: + // 1 = in, 2 = cm + mInfo.X_density = DEFAULT_X_DENSITY; // Horizontal pixel density + mInfo.Y_density = DEFAULT_Y_DENSITY; // Vertical pixel density + + // Set compress quality + jpeg_set_quality(&mInfo, quality, TRUE); + + mFormat = format; + + // Setup scanline buffer + mScanlineBuflen = width * components; + mScanlineBytesRemaining = mScanlineBuflen; + mScanlineBuf = (JSAMPLE *) (mInfo.mem->alloc_small)( + reinterpret_cast<j_common_ptr>(&mInfo), JPOOL_PERMANENT, + mScanlineBuflen * sizeof(JSAMPLE)); + mScanlineIter = mScanlineBuf; + + // Start compression + jpeg_start_compress(&mInfo, TRUE); + mSetup = true; + return J_SUCCESS; +} + +int32_t JpegWriter::write(int8_t* bytes, int32_t length) { + if (!mSetup) { + return J_ERROR_FATAL; + } + if (mFinished) { + return 0; + } + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + if (length < 0 || bytes == NULL) { + return J_ERROR_BAD_ARGS; + } + + int32_t total_length = length; + JSAMPROW row_pointer[1]; + while (mInfo.next_scanline < mInfo.image_height) { + if (length < mScanlineBytesRemaining) { + // read partial scanline and return + memcpy((void*) mScanlineIter, (void*) bytes, + length * sizeof(int8_t)); + mScanlineBytesRemaining -= length; + mScanlineIter += length; + return total_length; + } else if (length > 0) { + // read full scanline + memcpy((void*) mScanlineIter, (void*) bytes, + mScanlineBytesRemaining * sizeof(int8_t)); + bytes += mScanlineBytesRemaining; + length -= mScanlineBytesRemaining; + mScanlineBytesRemaining = 0; + } + // Do in-place pixel formatting + formatPixels(static_cast<uint8_t*>(mScanlineBuf), mScanlineBuflen); + row_pointer[0] = mScanlineBuf; + // Do compression + if (jpeg_write_scanlines(&mInfo, row_pointer, 1) != 1) { + return J_ERROR_FATAL; + } + // Reset scanline buffer + mScanlineBytesRemaining = mScanlineBuflen; + mScanlineIter = mScanlineBuf; + } + jpeg_finish_compress(&mInfo); + mFinished = true; + return total_length - length; +} + +// Does in-place pixel formatting +void JpegWriter::formatPixels(uint8_t* buf, int32_t len) { + // Assumes len is a multiple of 4 for RGBA and ABGR pixels. + assert((len % 4) == 0); + uint8_t* d = buf; + switch (mFormat) { + case Jpeg_Config::FORMAT_RGBA: { + // Strips alphas + for (int i = 0; i < len / 4; ++i, buf += 4) { + *d++ = buf[0]; + *d++ = buf[1]; + *d++ = buf[2]; + } + break; + } + case Jpeg_Config::FORMAT_ABGR: { + // Strips alphas and flips endianness + if (len / 4 >= 1) { + *d++ = buf[3]; + uint8_t tmp = *d; + *d++ = buf[2]; + *d++ = tmp; + } + for (int i = 1; i < len / 4; ++i, buf += 4) { + *d++ = buf[3]; + *d++ = buf[2]; + *d++ = buf[1]; + } + break; + } + default: { + // Do nothing + break; + } + } +} + +void JpegWriter::updateEnv(JNIEnv *env) { + UpdateDstEnv(&mInfo, env); +} + +int32_t JpegWriter::reset() { + // Set jump address for error handling + if (setjmp(mErrorManager.setjmp_buf)) { + return J_ERROR_FATAL; + } + // Clean up global java references + CleanDst(&mInfo); + // Wipe compress struct, free memory pools + jpeg_destroy_compress(&mInfo); + mFinished = false; + mSetup = false; + return J_SUCCESS; +} diff --git a/jni_jpegstream/src/jpeg_writer.h b/jni_jpegstream/src/jpeg_writer.h new file mode 100644 index 000000000..bd9a42d32 --- /dev/null +++ b/jni_jpegstream/src/jpeg_writer.h @@ -0,0 +1,83 @@ +/* + * 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. + */ +#ifndef JPEG_WRITER_H_ +#define JPEG_WRITER_H_ + +#include "jerr_hook.h" +#include "jni_defines.h" +#include "jpeg_config.h" + +#include <stdint.h> + +/** + * JpegWriter wraps libjpeg's compression functionality and a + * java OutputStream object. Write calls result in input data + * being compressed and written to the OuputStream. + */ +class JpegWriter { +public: + JpegWriter(); + ~JpegWriter(); + + /** + * Call setup with a valid OutputStream reference, bitmap height and + * width, pixel format, and compression quality in range (0, 100]. + * + * Returns J_SUCCESS on success or a negative error code. + */ + int32_t setup(JNIEnv *env, jobject out, int32_t width, int32_t height, + Jpeg_Config::Format format, int32_t quality); + + /** + * Compresses bytes from the input buffer. + * + * ***This method will result in bytes being written to the OutputStream*** + * + * Returns J_SUCCESS on success or a negative error code. + */ + int32_t write(int8_t* bytes, int32_t length); + + /** + * Updates the environment pointer. Call this before write or reset + * in any jni function call. + */ + void updateEnv(JNIEnv *env); + + /** + * Frees any java global references held by the JpegWriter, destroys + * the compress structure, and frees allocations in libjpeg's pools. + */ + int32_t reset(); + + static const int32_t DEFAULT_X_DENSITY; + static const int32_t DEFAULT_Y_DENSITY; + static const int32_t DEFAULT_DENSITY_UNIT; +private: + void formatPixels(uint8_t* buf, int32_t len); + struct jpeg_compress_struct mInfo; + ErrManager mErrorManager; + + JSAMPLE* mScanlineBuf; + JSAMPLE* mScanlineIter; + int32_t mScanlineBuflen; + int32_t mScanlineBytesRemaining; + + Jpeg_Config::Format mFormat; + bool mFinished; + bool mSetup; +}; + +#endif // JPEG_WRITER_H_ diff --git a/jni_jpegstream/src/jpegstream.cpp b/jni_jpegstream/src/jpegstream.cpp new file mode 100644 index 000000000..3b9a6830b --- /dev/null +++ b/jni_jpegstream/src/jpegstream.cpp @@ -0,0 +1,377 @@ +/* + * 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. + */ + +#include "error_codes.h" +#include "jni_defines.h" +#include "jpeg_writer.h" +#include "jpeg_reader.h" +#include "jpeg_config.h" +#include "outputstream_wrapper.h" +#include "inputstream_wrapper.h" + +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +static jint OutputStream_setup(JNIEnv* env, jobject thiz, jobject out, + jint width, jint height, jint format, jint quality) { + // Get a reference to this object's class + jclass thisClass = env->GetObjectClass(thiz); + if (env->ExceptionCheck() || thisClass == NULL) { + return J_EXCEPTION; + } + // Get field for storing C pointer + jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J"); + if (NULL == fidNumber || env->ExceptionCheck()) { + return J_EXCEPTION; + } + + // Check size + if (width <= 0 || height <= 0) { + return J_ERROR_BAD_ARGS; + } + Jpeg_Config::Format fmt = static_cast<Jpeg_Config::Format>(format); + // Check format + switch (fmt) { + case Jpeg_Config::FORMAT_GRAYSCALE: + case Jpeg_Config::FORMAT_RGB: + case Jpeg_Config::FORMAT_RGBA: + case Jpeg_Config::FORMAT_ABGR: + break; + default: + return J_ERROR_BAD_ARGS; + } + + uint32_t w = static_cast<uint32_t>(width); + uint32_t h = static_cast<uint32_t>(height); + int32_t q = static_cast<int32_t>(quality); + // Clamp quality to (0, 100] + q = (q > 100) ? 100 : ((q < 1) ? 1 : q); + + JpegWriter* w_ptr = new JpegWriter(); + + // Do JpegWriter setup. + int32_t errorFlag = w_ptr->setup(env, out, w, h, fmt, q); + if (env->ExceptionCheck() || errorFlag != J_SUCCESS) { + delete w_ptr; + return errorFlag; + } + + // Store C pointer for writer + env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(w_ptr)); + if (env->ExceptionCheck()) { + delete w_ptr; + return J_EXCEPTION; + } + return J_SUCCESS; +} + +static jint InputStream_setup(JNIEnv* env, jobject thiz, jobject dimens, + jobject in, jint format) { + // Get a reference to this object's class + jclass thisClass = env->GetObjectClass(thiz); + if (env->ExceptionCheck() || thisClass == NULL) { + return J_EXCEPTION; + } + jmethodID setMethod = NULL; + + // Get dimensions object setter method + if (dimens != NULL) { + jclass pointClass = env->GetObjectClass(dimens); + if (env->ExceptionCheck() || pointClass == NULL) { + return J_EXCEPTION; + } + setMethod = env->GetMethodID(pointClass, "set", "(II)V"); + if (env->ExceptionCheck() || setMethod == NULL) { + return J_EXCEPTION; + } + } + // Get field for storing C pointer + jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J"); + if (NULL == fidNumber || env->ExceptionCheck()) { + return J_EXCEPTION; + } + Jpeg_Config::Format fmt = static_cast<Jpeg_Config::Format>(format); + // Check format + switch (fmt) { + case Jpeg_Config::FORMAT_GRAYSCALE: + case Jpeg_Config::FORMAT_RGB: + case Jpeg_Config::FORMAT_RGBA: + case Jpeg_Config::FORMAT_ABGR: + break; + default: + return J_ERROR_BAD_ARGS; + } + + JpegReader* r_ptr = new JpegReader(); + int32_t w = 0, h = 0; + // Do JpegReader setup. + int32_t errorFlag = r_ptr->setup(env, in, &w, &h, fmt); + if (env->ExceptionCheck() || errorFlag != J_SUCCESS) { + delete r_ptr; + return errorFlag; + } + + // Set dimensions to return + if (dimens != NULL) { + env->CallVoidMethod(dimens, setMethod, static_cast<jint>(w), + static_cast<jint>(h)); + if (env->ExceptionCheck()) { + delete r_ptr; + return J_EXCEPTION; + } + } + // Store C pointer for reader + env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(r_ptr)); + if (env->ExceptionCheck()) { + delete r_ptr; + return J_EXCEPTION; + } + return J_SUCCESS; +} + +static JpegWriter* getWPtr(JNIEnv* env, jobject thiz, jfieldID* fid) { + jclass thisClass = env->GetObjectClass(thiz); + if (env->ExceptionCheck() || thisClass == NULL) { + return NULL; + } + jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J"); + if (NULL == fidNumber || env->ExceptionCheck()) { + return NULL; + } + jlong ptr = env->GetLongField(thiz, fidNumber); + if (env->ExceptionCheck()) { + return NULL; + } + // Get writer C pointer out of java field. + JpegWriter* w_ptr = reinterpret_cast<JpegWriter*>(ptr); + if (fid != NULL) { + *fid = fidNumber; + } + return w_ptr; +} + +static JpegReader* getRPtr(JNIEnv* env, jobject thiz, jfieldID* fid) { + jclass thisClass = env->GetObjectClass(thiz); + if (env->ExceptionCheck() || thisClass == NULL) { + return NULL; + } + jfieldID fidNumber = env->GetFieldID(thisClass, "JNIPointer", "J"); + if (NULL == fidNumber || env->ExceptionCheck()) { + return NULL; + } + jlong ptr = env->GetLongField(thiz, fidNumber); + if (env->ExceptionCheck()) { + return NULL; + } + // Get reader C pointer out of java field. + JpegReader* r_ptr = reinterpret_cast<JpegReader*>(ptr); + if (fid != NULL) { + *fid = fidNumber; + } + return r_ptr; +} + +static void OutputStream_cleanup(JNIEnv* env, jobject thiz) { + jfieldID fidNumber = NULL; + JpegWriter* w_ptr = getWPtr(env, thiz, &fidNumber); + if (w_ptr == NULL) { + return; + } + // Update environment + w_ptr->updateEnv(env); + // Destroy writer object + delete w_ptr; + w_ptr = NULL; + // Set the java field to null + env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(w_ptr)); +} + +static void InputStream_cleanup(JNIEnv* env, jobject thiz) { + jfieldID fidNumber = NULL; + JpegReader* r_ptr = getRPtr(env, thiz, &fidNumber); + if (r_ptr == NULL) { + return; + } + // Update environment + r_ptr->updateEnv(env); + // Destroy the reader object + delete r_ptr; + r_ptr = NULL; + // Set the java field to null + env->SetLongField(thiz, fidNumber, reinterpret_cast<jlong>(r_ptr)); +} + +static jint OutputStream_writeInputBytes(JNIEnv* env, jobject thiz, + jbyteArray inBuffer, jint offset, jint inCount) { + JpegWriter* w_ptr = getWPtr(env, thiz, NULL); + if (w_ptr == NULL) { + return J_EXCEPTION; + } + // Pin input buffer + jbyte* in_buf = (jbyte*) env->GetByteArrayElements(inBuffer, 0); + if (env->ExceptionCheck() || in_buf == NULL) { + return J_EXCEPTION; + } + + int8_t* in_bytes = static_cast<int8_t*>(in_buf); + int32_t in_len = static_cast<int32_t>(inCount); + int32_t off = static_cast<int32_t>(offset); + in_bytes += off; + int32_t written = 0; + + // Update environment + w_ptr->updateEnv(env); + // Write out and unpin buffer. + written = w_ptr->write(in_bytes, in_len); + env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_ABORT); + return written; +} + +static jint InputStream_readDecodedBytes(JNIEnv* env, jobject thiz, + jbyteArray inBuffer, jint offset, jint inCount) { + JpegReader* r_ptr = getRPtr(env, thiz, NULL); + if (r_ptr == NULL) { + return J_EXCEPTION; + } + // Pin input buffer + jbyte* in_buf = (jbyte*) env->GetByteArrayElements(inBuffer, 0); + if (env->ExceptionCheck() || in_buf == NULL) { + return J_EXCEPTION; + } + int8_t* in_bytes = static_cast<int8_t*>(in_buf); + int32_t in_len = static_cast<int32_t>(inCount); + int32_t off = static_cast<int32_t>(offset); + int32_t read = 0; + + // Update environment + r_ptr->updateEnv(env); + // Read into buffer + read = r_ptr->read(in_bytes, off, in_len); + + // Unpin buffer + if (read < 0) { + env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_ABORT); + } else { + env->ReleaseByteArrayElements(inBuffer, in_buf, JNI_COMMIT); + } + return read; +} + +static jint InputStream_skipDecodedBytes(JNIEnv* env, jobject thiz, + jint bytes) { + if (bytes <= 0) { + return J_ERROR_BAD_ARGS; + } + JpegReader* r_ptr = getRPtr(env, thiz, NULL); + if (r_ptr == NULL) { + return J_EXCEPTION; + } + + // Update environment + r_ptr->updateEnv(env); + int32_t skip = 0; + // Read with null buffer to skip + skip = r_ptr->read(NULL, 0, bytes); + return skip; +} + +static const char *outClassPathName = + "com/android/gallery3d/jpegstream/JPEGOutputStream"; +static const char *inClassPathName = + "com/android/gallery3d/jpegstream/JPEGInputStream"; + +static JNINativeMethod writeMethods[] = { { "setup", + "(Ljava/io/OutputStream;IIII)I", (void*) OutputStream_setup }, { + "cleanup", "()V", (void*) OutputStream_cleanup }, { "writeInputBytes", + "([BII)I", (void*) OutputStream_writeInputBytes } }; + +static JNINativeMethod readMethods[] = { { "setup", + "(Landroid/graphics/Point;Ljava/io/InputStream;I)I", + (void*) InputStream_setup }, { "cleanup", "()V", + (void*) InputStream_cleanup }, { "readDecodedBytes", "([BII)I", + (void*) InputStream_readDecodedBytes }, { "skipDecodedBytes", "(I)I", + (void*) InputStream_skipDecodedBytes } }; + +static int registerNativeMethods(JNIEnv* env, const char* className, + JNINativeMethod* gMethods, int numMethods) { + jclass clazz; + clazz = env->FindClass(className); + if (clazz == NULL) { + LOGE("Native registration unable to find class '%s'", className); + return JNI_FALSE; + } + if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) { + LOGE("RegisterNatives failed for '%s'", className); + return JNI_FALSE; + } + return JNI_TRUE; +} + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOGE("Error: GetEnv failed in JNI_OnLoad"); + return -1; + } + if (!registerNativeMethods(env, outClassPathName, writeMethods, + sizeof(writeMethods) / sizeof(writeMethods[0]))) { + LOGE("Error: could not register native methods for JPEGOutputStream"); + return -1; + } + if (!registerNativeMethods(env, inClassPathName, readMethods, + sizeof(readMethods) / sizeof(readMethods[0]))) { + LOGE("Error: could not register native methods for JPEGInputStream"); + return -1; + } + // cache method IDs for OutputStream + jclass outCls = env->FindClass("java/io/OutputStream"); + if (outCls == NULL) { + LOGE("Unable to find class 'OutputStream'"); + return -1; + } + jmethodID cachedWriteFun = env->GetMethodID(outCls, "write", "([BII)V"); + if (cachedWriteFun == NULL) { + LOGE("Unable to find write function in class 'OutputStream'"); + return -1; + } + OutputStreamWrapper::setWriteMethodID(cachedWriteFun); + + // cache method IDs for InputStream + jclass inCls = env->FindClass("java/io/InputStream"); + if (inCls == NULL) { + LOGE("Unable to find class 'InputStream'"); + return -1; + } + jmethodID cachedReadFun = env->GetMethodID(inCls, "read", "([BII)I"); + if (cachedReadFun == NULL) { + LOGE("Unable to find read function in class 'InputStream'"); + return -1; + } + jmethodID cachedSkipFun = env->GetMethodID(inCls, "skip", "(J)J"); + if (cachedSkipFun == NULL) { + LOGE("Unable to find skip function in class 'InputStream'"); + return -1; + } + InputStreamWrapper::setReadSkipMethodIDs(cachedReadFun, cachedSkipFun); + return JNI_VERSION_1_6; +} + +#ifdef __cplusplus +} +#endif diff --git a/jni_jpegstream/src/outputstream_wrapper.cpp b/jni_jpegstream/src/outputstream_wrapper.cpp new file mode 100644 index 000000000..0639b6eea --- /dev/null +++ b/jni_jpegstream/src/outputstream_wrapper.cpp @@ -0,0 +1,49 @@ +/* + * 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. + */ + +#include "outputstream_wrapper.h" +#include "error_codes.h" + +jmethodID OutputStreamWrapper::sWriteID = NULL; + +int32_t OutputStreamWrapper::write(int32_t length, int32_t offset) { + if (offset < 0 || length < 0 || (offset + length) > getBufferSize()) { + return J_ERROR_BAD_ARGS; + } + mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_COMMIT); + mBytes = NULL; + if (mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + if (sWriteID == NULL) { + LOGE("Uninitialized method ID for OutputStream write function."); + return J_ERROR_FATAL; + } + // Call OutputStream write with byte array. + mEnv->CallVoidMethod(mStream, sWriteID, mByteArray, offset, length); + if (mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + mBytes = mEnv->GetByteArrayElements(mByteArray, NULL); + if (mBytes == NULL || mEnv->ExceptionCheck()) { + return J_EXCEPTION; + } + return J_SUCCESS; +} + +void OutputStreamWrapper::setWriteMethodID(jmethodID id) { + sWriteID = id; +} diff --git a/jni_jpegstream/src/outputstream_wrapper.h b/jni_jpegstream/src/outputstream_wrapper.h new file mode 100644 index 000000000..9b8b0071b --- /dev/null +++ b/jni_jpegstream/src/outputstream_wrapper.h @@ -0,0 +1,35 @@ +/* + * 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. + */ + +#ifndef OUTPUTSTREAM_WRAPPER_H_ +#define OUTPUTSTREAM_WRAPPER_H_ + +#include "jni_defines.h" +#include "stream_wrapper.h" + +#include <stdint.h> + +class OutputStreamWrapper : public StreamWrapper { +public: + virtual int32_t write(int32_t length, int32_t offset); + + // Call this in JNI_OnLoad to cache write method + static void setWriteMethodID(jmethodID id); +protected: + static jmethodID sWriteID; +}; + +#endif // OUTPUTSTREAM_WRAPPER_H_ diff --git a/jni_jpegstream/src/stream_wrapper.cpp b/jni_jpegstream/src/stream_wrapper.cpp new file mode 100644 index 000000000..049d84f67 --- /dev/null +++ b/jni_jpegstream/src/stream_wrapper.cpp @@ -0,0 +1,97 @@ +/* + * 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. + */ + +#include "stream_wrapper.h" + +const int32_t StreamWrapper::END_OF_STREAM = -1; +const int32_t StreamWrapper::DEFAULT_BUFFER_SIZE = 1 << 16; // 64Kb + +StreamWrapper::StreamWrapper() : mEnv(NULL), + mStream(NULL), + mByteArray(NULL), + mBytes(NULL), + mByteArrayLen(0) {} + +StreamWrapper::~StreamWrapper() { + cleanup(); +} + +void StreamWrapper::updateEnv(JNIEnv *env) { + if (env == NULL) { + LOGE("Cannot update StreamWrapper with a null JNIEnv pointer!"); + return; + } + mEnv = env; +} + +bool StreamWrapper::init(JNIEnv *env, jobject stream) { + if (mEnv != NULL) { + LOGW("StreamWrapper already initialized!"); + return false; + } + mEnv = env; + mStream = env->NewGlobalRef(stream); + if (mStream == NULL || env->ExceptionCheck()) { + cleanup(); + return false; + } + mByteArrayLen = DEFAULT_BUFFER_SIZE; + jbyteArray tmp = env->NewByteArray(getBufferSize()); + if (tmp == NULL || env->ExceptionCheck()){ + cleanup(); + return false; + } + mByteArray = reinterpret_cast<jbyteArray>(env->NewGlobalRef(tmp)); + if (mByteArray == NULL || env->ExceptionCheck()){ + cleanup(); + return false; + } + mBytes = env->GetByteArrayElements(mByteArray, NULL); + if (mBytes == NULL || env->ExceptionCheck()){ + cleanup(); + return false; + } + return true; +} + +void StreamWrapper::cleanup() { + if (mEnv != NULL) { + if (mStream != NULL) { + mEnv->DeleteGlobalRef(mStream); + mStream = NULL; + } + if (mByteArray != NULL) { + if (mBytes != NULL) { + mEnv->ReleaseByteArrayElements(mByteArray, mBytes, JNI_ABORT); + mBytes = NULL; + } + mEnv->DeleteGlobalRef(mByteArray); + mByteArray = NULL; + } else { + mBytes = NULL; + } + mByteArrayLen = 0; + mEnv = NULL; + } +} + +int32_t StreamWrapper::getBufferSize() { + return mByteArrayLen; +} + +jbyte* StreamWrapper::getBufferPtr() { + return mBytes; +} diff --git a/jni_jpegstream/src/stream_wrapper.h b/jni_jpegstream/src/stream_wrapper.h new file mode 100644 index 000000000..e036a9145 --- /dev/null +++ b/jni_jpegstream/src/stream_wrapper.h @@ -0,0 +1,44 @@ +/* + * 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. + */ + +#ifndef STREAM_WRAPPER_H_ +#define STREAM_WRAPPER_H_ + +#include "jni_defines.h" + +#include <stdint.h> + +class StreamWrapper { +public: + StreamWrapper(); + virtual ~StreamWrapper(); + virtual void updateEnv(JNIEnv *env); + virtual bool init(JNIEnv *env, jobject stream); + virtual void cleanup(); + virtual int32_t getBufferSize(); + virtual jbyte* getBufferPtr(); + + const static int32_t DEFAULT_BUFFER_SIZE; + const static int32_t END_OF_STREAM; +protected: + JNIEnv *mEnv; + jobject mStream; + jbyteArray mByteArray; + jbyte* mBytes; + int32_t mByteArrayLen; +}; + +#endif // STREAM_WRAPPER_H_ diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java new file mode 100644 index 000000000..2e56145cf --- /dev/null +++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java @@ -0,0 +1,81 @@ +/* + * 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.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.os.Environment; +import android.util.Log; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.tests.R; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public class JpegStreamReaderTest extends JpegStreamTestCase { + public static final String TAG = "JpegStreamReaderTest"; + private JPEGInputStream mStream; + private Bitmap mBitmap; + + public JpegStreamReaderTest(int imageRes) { + super(imageRes); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mBitmap = BitmapFactory.decodeStream(getImageInputStream()); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + Utils.closeSilently(mStream); + mStream = null; + if (mBitmap != null) { + mBitmap.recycle(); + mBitmap = null; + } + } + + public void testBasicReads() throws Exception { + + // Setup input stream. + mStream = new JPEGInputStream(reopenFileStream(), JpegConfig.FORMAT_RGBA); + Point dimens = mStream.getDimensions(); + + // Read whole stream into array. + byte[] bytes = new byte[dimens.x * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA) * dimens.y]; + assertTrue(mStream.read(bytes, 0, bytes.length) == bytes.length); + + // Set pixels in bitmap + Bitmap test = Bitmap.createBitmap(dimens.x, dimens.y, Bitmap.Config.ARGB_8888); + ByteBuffer buf = ByteBuffer.wrap(bytes); + test.copyPixelsFromBuffer(buf); + assertTrue(test.getWidth() == mBitmap.getWidth() && test.getHeight() == mBitmap.getHeight()); + assertTrue(mStream.read(bytes, 0, bytes.length) == -1); + } + + // TODO : more tests +} diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java new file mode 100644 index 000000000..ed3b08aaa --- /dev/null +++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java @@ -0,0 +1,71 @@ +/* + * 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.content.res.Resources; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.gallery3d.common.Utils; + +import java.io.InputStream; + +public class JpegStreamTestCase extends InstrumentationTestCase { + public static final String TAG = "JpegStreamTestCase"; + + private static final String RES_ID_TITLE = "Resource ID: %x"; + + private InputStream mImageInputStream; + private final int mImageResourceId; + + public JpegStreamTestCase(int imageRes) { + mImageResourceId = imageRes; + } + + protected InputStream getImageInputStream() { + return mImageInputStream; + } + + @Override + public void setUp() throws Exception { + Log.d(TAG, "doing setUp..."); + mImageInputStream = reopenFileStream(); + } + + @Override + public void tearDown() throws Exception { + Log.d(TAG, "doing tearDown..."); + Utils.closeSilently(mImageInputStream); + mImageInputStream = null; + } + + protected String getImageTitle() { + return String.format(RES_ID_TITLE, mImageResourceId); + } + + protected InputStream reopenFileStream() throws Exception { + return openResource(mImageResourceId); + } + + protected InputStream openResource(int resourceID) throws Exception { + try { + Resources res = getInstrumentation().getContext().getResources(); + return res.openRawResource(resourceID); + } catch (Exception e) { + throw new Exception(getImageTitle(), e); + } + } +} diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java new file mode 100644 index 000000000..2afaf39eb --- /dev/null +++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java @@ -0,0 +1,78 @@ +/* + * 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.test.InstrumentationTestRunner; +import android.test.InstrumentationTestSuite; +import android.util.Log; + +import com.android.gallery3d.exif.ExifTestRunner; +import com.android.gallery3d.tests.R; + +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class JpegStreamTestRunner extends InstrumentationTestRunner { + private static final String TAG = "JpegStreamTestRunner"; + + private static final int[] IMG_RESOURCE = { + R.raw.galaxy_nexus + }; + + @Override + public TestSuite getAllTests() { + TestSuite suite = new InstrumentationTestSuite(this); + addAllTestsFromTestCase(JpegStreamReaderTest.class, suite); + addAllTestsFromTestCase(JpegStreamWriterTest.class, suite); + return suite; + } + + private void addAllTestsFromTestCase(Class<? extends JpegStreamTestCase> testClass, + TestSuite suite) { + for (Method method : testClass.getDeclaredMethods()) { + if (method.getName().startsWith("test") && method.getParameterTypes().length == 0) { + for (int i = 0; i < IMG_RESOURCE.length; i++) { + TestCase test; + try { + test = testClass.getDeclaredConstructor(int.class). + newInstance(IMG_RESOURCE[i]); + test.setName(method.getName()); + suite.addTest(test); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to create test case", e); + } catch (InstantiationException e) { + Log.e(TAG, "Failed to create test case", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Failed to create test case", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Failed to create test case", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Failed to create test case", e); + } + } + } + } + } + + @Override + public ClassLoader getLoader() { + return ExifTestRunner.class.getClassLoader(); + } +} diff --git a/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java b/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java new file mode 100644 index 000000000..befba4ccd --- /dev/null +++ b/tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java @@ -0,0 +1,138 @@ +/* + * 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.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Environment; +import android.util.Log; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.tests.R; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; + +public class JpegStreamWriterTest extends JpegStreamTestCase { + public static final String TAG = "JpegStreamWriterTest"; + private JPEGOutputStream mStream; + private ByteArrayOutputStream mWrappedStream; + private Bitmap mBitmap; + private Bitmap mControl; + + // galaxy_nexus.jpg image compressed with q=20 + private static final int CONTROL_RID = R.raw.jpeg_control; + + public JpegStreamWriterTest(int imageRes) { + super(imageRes); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + mBitmap = BitmapFactory.decodeStream(getImageInputStream()); + mControl = BitmapFactory.decodeStream(openResource(CONTROL_RID)); + mWrappedStream = new ByteArrayOutputStream(); + + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + Utils.closeSilently(mStream); + Utils.closeSilently(mWrappedStream); + mWrappedStream = null; + mStream = null; + if (mBitmap != null) { + mBitmap.recycle(); + mBitmap = null; + } + if (mControl != null) { + mControl.recycle(); + mControl = null; + } + } + + public void testBasicWrites() throws Exception { + assertTrue(mBitmap != null); + int width = mBitmap.getWidth(); + int height = mBitmap.getHeight(); + mStream = new JPEGOutputStream(mWrappedStream, width, + height, 20, JpegConfig.FORMAT_RGBA); + + // Put bitmap pixels into a byte array (format is RGBA). + int rowLength = width * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA); + int size = height * rowLength; + byte[] byteArray = new byte[size]; + ByteBuffer buf = ByteBuffer.wrap(byteArray); + mBitmap.copyPixelsToBuffer(buf); + + // Write out whole array + mStream.write(byteArray, 0, byteArray.length); + mStream.close(); + + // Get compressed jpeg output + byte[] compressed = mWrappedStream.toByteArray(); + + // Check jpeg + ByteArrayInputStream inStream = new ByteArrayInputStream(compressed); + Bitmap test = BitmapFactory.decodeStream(inStream); + assertTrue(test != null); + assertTrue(test.sameAs(mControl)); + } + + public void testStreamingWrites() throws Exception { + assertTrue(mBitmap != null); + int width = mBitmap.getWidth(); + int height = mBitmap.getHeight(); + mStream = new JPEGOutputStream(mWrappedStream, width, + height, 20, JpegConfig.FORMAT_RGBA); + + // Put bitmap pixels into a byte array (format is RGBA). + int rowLength = width * StreamUtils.pixelSize(JpegConfig.FORMAT_RGBA); + int size = height * rowLength; + byte[] byteArray = new byte[size]; + ByteBuffer buf = ByteBuffer.wrap(byteArray); + mBitmap.copyPixelsToBuffer(buf); + + // Write array in chunks + int chunkSize = rowLength / 3; + int written = 0; + while (written < size) { + if (written + chunkSize > size) { + chunkSize = size - written; + } + mStream.write(byteArray, written, chunkSize); + written += chunkSize; + } + mStream.close(); + + // Get compressed jpeg output + byte[] compressed = mWrappedStream.toByteArray(); + + // Check jpeg + ByteArrayInputStream inStream = new ByteArrayInputStream(compressed); + Bitmap test = BitmapFactory.decodeStream(inStream); + assertTrue(test != null); + assertTrue(test.sameAs(mControl)); + } + + // TODO : more tests +} |