This library provides functionality to decode JPEG2000 images on Android.
Historically, native image decoders have been a significant security risk. This project aims to securely perform JPEG2000 decoding within an isolated sandbox environment by running OpenJPEG as WebAssembly (WASM).
- JPEG2000 Decoding: Supports decoding of JPEG2000 images on Android devices.
- Powered by OPENJPEG: Utilizes the OpenJPEG library for robust and efficient decoding.
- WASM & Jetpack JavaScript Engine: The native library is compiled to WebAssembly (WASM) and executed using the Jetpack JavaScript Engine.
- Enhanced Security: By running within the WASM engine's sandbox, the decoding process is isolated, offering a relatively higher level of safety compared to direct native execution.
The binaries are available on Maven Central.
To install the library, add the dependency to your module's build.gradle.kts (Kotlin DSL) or build.gradle (Groovy DSL).
implementation("dev.keiji.jp2k:jp2k-decoder-android:0.3.0")implementation 'dev.keiji.jp2k:jp2k-decoder-android:0.3.0'Jp2kDecoder is designed for use with Kotlin Coroutines. It implements AutoCloseable, so it can be used with the use block for automatic resource management.
val context: Context = ... // Application Context
val jp2kBytes: ByteArray = ... // JPEG2000 image data
val decoder = Jp2kDecoder(Config())
decoder.init(context)
val bitmap = decoder.decodeImage(jp2kBytes)
decoder.close()Or using use:
Jp2kDecoder(Config()).use { decoder ->
decoder.init(context)
val bitmap = decoder.decodeImage(jp2kBytes)
// Use bitmap
}Jp2kDecoderAsync provides a callback-based API, making it suitable for Java or non-coroutine environments.
Context context = ...; // Application Context
byte[] jp2kBytes = ...; // JPEG2000 image data
Jp2kDecoderAsync decoder = new Jp2kDecoderAsync(new Config(), Executors.newSingleThreadExecutor());
decoder.init(context, new Callback<Unit>() {
@Override
public void onSuccess(Unit result) {
decoder.decodeImage(jp2kBytes, new Callback<Bitmap>() {
@Override
public void onSuccess(Bitmap bitmap) {
// Use bitmap
decoder.close();
}
@Override
public void onError(Exception e) {
// Handle decode error
decoder.close();
}
});
}
@Override
public void onError(Exception e) {
// Handle init error
}
});If you plan to perform multiple operations on the same image (e.g., getting size first, then decoding, or decoding different regions), it is efficient to transfer the image data to the WASM environment once.
val jp2kBytes: ByteArray = ...
decoder.precache(jp2kBytes)
// Subsequent calls do not need to pass the byte array
val size = decoder.getSize()
val bitmap = decoder.decodeImage()You can retrieve the dimensions of the image without fully decoding it.
// Using byte array
val size = decoder.getSize(jp2kBytes)
println("Width: ${size.width}, Height: ${size.height}")
// Or using precached data
decoder.precache(jp2kBytes)
val size = decoder.getSize()You can decode a specific region of the image by specifying the coordinates (left, top, right, bottom).
// Decode a region
val bitmap = decoder.decodeImage(jp2kBytes, 100, 100, 300, 300)
// Or using precached data
decoder.precache(jp2kBytes)
val bitmap = decoder.decodeImage(100, 100, 300, 300)
// Using Rect
val rect = Rect(100, 100, 300, 300)
// With byte array
val bitmap = decoder.decodeImage(jp2kBytes, rect)
// Or with precached data
decoder.precache(jp2kBytes)
val bitmap = decoder.decodeImage(rect)You can also specify the region using ratios (0.0 - 1.0).
// Decode a region (ratio)
val bitmap = decoder.decodeImage(jp2kBytes, 0.0f, 0.0f, 0.5f, 0.5f)
// Or using precached data
decoder.precache(jp2kBytes)
val bitmap = decoder.decodeImage(0.0f, 0.0f, 0.5f, 0.5f)
// Using RectF
val rectF = RectF(0.0f, 0.0f, 0.5f, 0.5f)
// With byte array
val bitmap = decoder.decodeImage(jp2kBytes, rectF)
// Or with precached data
val bitmap = decoder.decodeImage(rectF)You can customize the decoder behavior by passing a Config object to the constructor.
| Parameter | Type | Default | Description |
|---|---|---|---|
maxPixels |
Int |
16,000,000 | The maximum number of pixels allowed in the decoded image. |
maxHeapSizeBytes |
Long |
512 MB | The maximum size of the heap in bytes allowed for the JavaScript sandbox. |
maxEvaluationReturnSizeBytes |
Int |
256 MB | The maximum size of the return value in bytes from JavaScript evaluation. |
logLevel |
Int? |
null |
The logging level (e.g., Log.DEBUG, Log.INFO). If null, logging is disabled. |
logger |
Logger |
AndroidLogger |
Custom Logger implementation to handle log messages. |
maxLogLines |
Int |
10 | The maximum number of log lines to output per message. Excess lines will be truncated. |
preferDirectBinaryTransfer |
Boolean |
true |
Whether to prefer direct binary transfer via provideNamedData when supported. |
When logLevel is set (e.g., Log.INFO), detailed logs regarding data transfers, performance metrics, and timing analysis are output to Logcat:
2026-08-12 19:42:17.170 Jp2kDecoder I Input binary length: 147041
2026-08-12 19:42:17.170 Jp2kDecoder I J2K expression: (async () => { ... })()
2026-08-12 19:42:17.468 Jp2kDecoder I Output encoded content length: 1638472 chars
2026-08-12 19:42:17.688 Jp2kDecoder I Output encoded content (64 chars per line):
Qk02wBIAAAAAADYAAAAoAAAAgAIAACD+//8BACAAAAAAAAAAAAAAAAAAAAAAAAAA
... (truncated 25593 lines) ...
QEgS/
2026-08-12 19:42:17.726 Jp2kDecoder I Output data length: 1228854 bytes
2026-08-12 19:42:17.727 Jp2kDecoder I Input transfer start delay (Kotlin -> JS start): 2.00 ms
2026-08-12 19:42:17.727 Jp2kDecoder I Output transfer delay (JS finish -> Kotlin receive): 20.00 ms
2026-08-12 19:42:17.727 Jp2kDecoder I Output Kotlin decode time: 37.97 ms
2026-08-12 19:42:17.728 Jp2kDecoder I Performance: inputSize=0B totalTime=545ms
dataTransferTime=261ms jsDecodeTime=0ms jsEncodeTime=144ms
wasmHeapSize=7MB outputImage=1228854B
2026-08-12 19:42:17.728 Jp2kDecoder I Pre-process: 0.0 ms, WASM: 95.0 ms, Post-process: 144.0 ms
2026-08-12 19:42:17.728 Jp2kDecoder I decodeImage() finished in 546 msec
| Log Message / Metric | Description (説明) |
|---|---|
Input binary length / Input data length |
Size of the input raw JPEG 2000 byte array in bytes (Kotlin側から入力したデータサイズ)。 |
Input encoded content length |
Length of the encoded string in characters when string-mediated transfer is used. |
Input encoded content (64 chars per line) |
Encoded string of input data (formatted 64 chars/line). Truncated if exceeding maxLogLines. |
Input JS decode time |
Time spent decoding input string payload to Uint8Array in JavaScript (ms). |
Input transfer start delay (Kotlin -> JS start) |
Overhead delay from Kotlin request initiation to JS execution start (ms). |
Output transfer delay (JS finish -> Kotlin receive) |
Overhead delay from JS execution completion to Kotlin result receipt (ms). |
Output encoded content length |
Length of the decoded output image payload (e.g. BMP) in characters (chars). |
Output encoded content (64 chars per line) |
Encoded output string (formatted 64 chars/line). Truncated if exceeding maxLogLines. |
Output Kotlin decode time |
Time spent decoding the return string payload in Kotlin (ms). |
Output data length |
Size of the decoded output bitmap bytes in bytes. |
Performance: ... / Pre-process / WASM / Post-process |
Breakdown of WASM execution, JS pre/post-processing, heap size, and total time. |
The library supports the following color formats for the output Bitmap. You can specify the format in decodeImage.
ColorFormat.ARGB8888(Default): High quality, 4 bytes per pixel. Supports transparency.ColorFormat.RGB565: Lower quality, 2 bytes per pixel. No transparency support.
The decoder manages its internal state to ensure thread safety and resource management. The states are:
Uninitialized: The initial state.Initializing:init()has been called and the JavaScript sandbox is starting.Initialized: Ready to decode images.Processing: Currently executing a task (e.g., decoding, checking memory usage).Releasing:release()orclose()has been called.Released: Resources have been freed. The decoder cannot be used anymore.
Calls to decodeImage are allowed only when the state is Initialized (or Processing for Async, which queues requests).
Ensure you have cloned the repository with submodules, or initialize them:
git submodule update --init --recursiveFirst, build the OpenJPEG library. This requires Emscripten to be installed and active in your environment.
mkdir -p openjpeg/build
cd openjpeg/build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DBUILD_CODEC=OFF
emmake make
cd ../..Compile the C wrapper and link it with the OpenJPEG library to create the WASM module.
emcc -O3 wrapper.c \
-I./openjpeg/src/lib/openjp2 \
-I./openjpeg/build/src/lib/openjp2 \
-L./openjpeg/build/bin \
-lopenjp2 \
-s WASM=1 \
-s STANDALONE_WASM \
--no-entry \
-s ALLOW_MEMORY_GROWTH=1 \
-s INITIAL_MEMORY=4194304 \
-s TOTAL_STACK=1048576 \
-s EXPORTED_FUNCTIONS='["_decodeToBmp", "_malloc", "_free", "_getLastError"]' \
-o openjpeg_core.wasmUnit tests for the C wrapper logic (e.g., BMP conversion) can be run without Emscripten using GCC or Clang.
bash test/run_tests.shTo measure the code coverage for Android unit tests, run the following command:
cd android
./gradlew lib:jacocoTestReportThe HTML report will be generated at android/lib/build/reports/jacoco/jacocoTestReport/html/index.html.
To measure the code coverage for the C/Native code (wrapper.c), run the following command:
./test/run_coverage.shThe summary will be printed to stdout.
To generate the API documentation (KDoc), run the following command:
cd android
./gradlew :lib:dokkaHtmlThe generated documentation will be available in android/lib/build/dokka/html.
For more details on the internal architecture, data flow, and state management, please refer to the Design Document.
Note: The design document is currently available in Japanese only.
To publish the library to Maven Central Portal, export the required environment variables and run the Gradle task.
export CENTRAL_PORTAL_USERNAME=<your-username>
export CENTRAL_PORTAL_PASSWORD=<your-password>
cd android
./gradlew publishAggregationToCentralPortalCopyright 2026 ARIYAMA Keiji
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.