OSDN Git Service

Added jpeg streaming classes.
authorRuben Brunk <rubenbrunk@google.com>
Sat, 29 Jun 2013 03:02:54 +0000 (20:02 -0700)
committerRuben Brunk <rubenbrunk@google.com>
Thu, 11 Jul 2013 20:41:29 +0000 (13:41 -0700)
- 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:
gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java [new file with mode: 0644]
gallerycommon/src/com/android/gallery3d/jpegstream/JPEGOutputStream.java [new file with mode: 0644]
gallerycommon/src/com/android/gallery3d/jpegstream/JpegConfig.java [new file with mode: 0644]
gallerycommon/src/com/android/gallery3d/jpegstream/StreamUtils.java [new file with mode: 0644]
jni_jpegstream/src/error_codes.h [new file with mode: 0644]
jni_jpegstream/src/inputstream_wrapper.cpp [new file with mode: 0644]
jni_jpegstream/src/inputstream_wrapper.h [new file with mode: 0644]
jni_jpegstream/src/jerr_hook.cpp [new file with mode: 0644]
jni_jpegstream/src/jerr_hook.h [new file with mode: 0644]
jni_jpegstream/src/jni_defines.h [new file with mode: 0644]
jni_jpegstream/src/jpeg_config.h [new file with mode: 0644]
jni_jpegstream/src/jpeg_hook.cpp [new file with mode: 0644]
jni_jpegstream/src/jpeg_hook.h [new file with mode: 0644]
jni_jpegstream/src/jpeg_reader.cpp [new file with mode: 0644]
jni_jpegstream/src/jpeg_reader.h [new file with mode: 0644]
jni_jpegstream/src/jpeg_writer.cpp [new file with mode: 0644]
jni_jpegstream/src/jpeg_writer.h [new file with mode: 0644]
jni_jpegstream/src/jpegstream.cpp [new file with mode: 0644]
jni_jpegstream/src/outputstream_wrapper.cpp [new file with mode: 0644]
jni_jpegstream/src/outputstream_wrapper.h [new file with mode: 0644]
jni_jpegstream/src/stream_wrapper.cpp [new file with mode: 0644]
jni_jpegstream/src/stream_wrapper.h [new file with mode: 0644]
tests/src/com/android/gallery3d/jpegstream/JpegStreamReaderTest.java [new file with mode: 0644]
tests/src/com/android/gallery3d/jpegstream/JpegStreamTestCase.java [new file with mode: 0644]
tests/src/com/android/gallery3d/jpegstream/JpegStreamTestRunner.java [new file with mode: 0644]
tests/src/com/android/gallery3d/jpegstream/JpegStreamWriterTest.java [new file with mode: 0644]

diff --git a/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java b/gallerycommon/src/com/android/gallery3d/jpegstream/JPEGInputStream.java
new file mode 100644 (file)
index 0000000..44ccd4c
--- /dev/null
@@ -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 (file)
index 0000000..c49d375
--- /dev/null
@@ -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 (file)
index 0000000..e514e3b
--- /dev/null
@@ -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 (file)
index 0000000..abd8f68
--- /dev/null
@@ -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 (file)
index 0000000..be55a00
--- /dev/null
@@ -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 (file)
index 0000000..98721b0
--- /dev/null
@@ -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 (file)
index 0000000..ed9942b
--- /dev/null
@@ -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 (file)
index 0000000..f8f864f
--- /dev/null
@@ -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 (file)
index 0000000..f2ba7cd
--- /dev/null
@@ -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 (file)
index 0000000..8c9bd04
--- /dev/null
@@ -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 (file)
index 0000000..a997552
--- /dev/null
@@ -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 (file)
index 0000000..cca54e4
--- /dev/null
@@ -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 (file)
index 0000000..b02bb34
--- /dev/null
@@ -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 (file)
index 0000000..4726b64
--- /dev/null
@@ -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 (file)
index 0000000..afde27b
--- /dev/null
@@ -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 (file)
index 0000000..4f78917
--- /dev/null
@@ -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 (file)
index 0000000..bd9a42d
--- /dev/null
@@ -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 (file)
index 0000000..3b9a683
--- /dev/null
@@ -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 (file)
index 0000000..0639b6e
--- /dev/null
@@ -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 (file)
index 0000000..9b8b007
--- /dev/null
@@ -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 (file)
index 0000000..049d84f
--- /dev/null
@@ -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 (file)
index 0000000..e036a91
--- /dev/null
@@ -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 (file)
index 0000000..2e56145
--- /dev/null
@@ -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 (file)
index 0000000..ed3b08a
--- /dev/null
@@ -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 (file)
index 0000000..2afaf39
--- /dev/null
@@ -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 (file)
index 0000000..befba4c
--- /dev/null
@@ -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
+}