
Decodes and renders multi-page TIFFs with a libtiff backend, offering page-level raster caching, affine transforms, distinct display/print resampling modes, and JPEG/WebP codec support.
A Kotlin Multiplatform library for decoding and rendering TIFF images — Android, iOS, and
JVM/desktop — including multi-page/multi-directory TIFFs, on top of libtiff.
// fd: an already-open, seekable file descriptor
TiffRenderer(TiffSource.fromFileDescriptor(fd, size)).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = createTiffBitmap(page.width, page.height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY)
}
}Android has no built-in TIFF decoder. PDF gets android.graphics.pdf.PdfRenderer, backed by
pdfium; TIFF gets nothing. TiffRenderer fills that gap. Its public API is deliberately modeled on
PdfRenderer's own shape — same method names, same lifecycle, same page/render-mode pattern — so
it's immediately familiar to any Android developer. It diverges only where the underlying reality
genuinely differs, documented inline where that happens.
TiffRenderer/TiffPage are one implementation shared across Android, iOS, and JVM — a single
platform-neutral C++ core, with only the innermost native call swapped per platform — so decoding
and rendering never drift between them. A few things worth knowing:
TiffPage. render() takes an optional destination clip and affine
transform.TiffPage#retainRaster() opts a page into decode caching, so rendering the same page at
multiple zoom levels doesn't redecode each time. Off by default — a cached raster is the page's
full uncompressed pixel grid, hundreds of MB for a large scan.TiffRenderMode.FOR_DISPLAY (bilinear + mip minification) and FOR_PRINT (nearest-neighbor)
are genuinely different resampling paths, not just labels.TiffRenderer(ParcelFileDescriptor) and
TiffPage#render(Bitmap, Rect?, Matrix?, TiffRenderMode) take the platform's own types
directly — no need to touch the cross-platform TiffSource/TiffBitmap/TiffRect/
TiffTransform wrappers at all.Versions before 2.0.0 were published to JitPack under
com.github.lucf15:TiffRenderer. That coordinate is no longer updated; migrate to the Maven Central one below.
Published to Maven Central as io.github.lucf15:tiffrenderer, a regular Kotlin Multiplatform
artifact (Android, JVM/desktop, iosArm64/iosSimulatorArm64):
// build.gradle.kts, in a Kotlin Multiplatform module
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>") // see the badge above for the latest
}
}
}A plain (non-KMP) Android app can depend on it directly instead:
// app/build.gradle.kts
dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>")
}tiffrenderer's Android target pulls in io.github.lucf15:tiffrenderer-native (the compiled
.sos) transitively; nothing extra to add for that.
Any Kotlin Multiplatform project can depend on the Maven coordinate above from its own
iosMain/iosArm64/iosSimulatorArm64 source sets — no separate iOS-specific artifact. A pure
Swift/Xcode project with no Kotlin involved still needs a real .framework/XCFramework, which
this coordinate alone doesn't provide (Maven has no such artifact shape); build one from source
the way sample/iosApp does, via :lib:core's embedAndSignAppleFrameworkForXcode Gradle task.
Same coordinate, jvm() target — works from a plain JVM project too, not just KMP:
// build.gradle.kts
dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>")
}Bundles native libraries for macOS (aarch64, x86_64), Linux (x86_64, aarch64), and Windows
(x86_64, aarch64); the right one is picked automatically at runtime based on the host JVM's
os.name/os.arch.
To build against a local clone instead of the published artifact, a Gradle composite build:
// settings.gradle.kts, in the app that wants to depend on this library
includeBuild("../path/to/TiffRenderer") {
dependencySubstitution {
substitute(module("io.github.lucf15:tiffrenderer")).using(project(":lib:core"))
}
}Building the library requires the Android NDK and CMake, versions pinned in
gradle/libs.versions.toml; Gradle fetches them automatically if they aren't already installed.
Building for iOS additionally requires Xcode. :lib:core's iOS cinterop tasks depend on
:lib:native's buildTiffCoreForIos task, so the native cross-compile
(lib/native/src/main/cpp/build-ios.sh) runs automatically as part of any Gradle iOS build — no
manual step needed.
libtiff, libjpeg, and libwebp are all vendored as git submodules. Clone with
--recurse-submodules, or run git submodule update --init --recursive afterwards, before
building any platform.
The snippet at the top of this README is the minimal case: open a source, open a page, render it. A few more common cases:
Rendering into a sub-region with a custom transform:
page.render(
bitmap,
destClip = TiffRect(0, 0, 512, 512),
transform = TiffTransform(floatArrayOf(2f, 0f, 0f, 0f, 2f, 0f)), // 2x scale
renderMode = TiffRenderMode.FOR_DISPLAY,
)Rendering the same page repeatedly (e.g. multiple zoom tiles) without redecoding each time:
renderer.openPage(0).use { page ->
page.retainRaster()
// every render() call below reuses the same decode
page.render(tile1, clip1, transform1, TiffRenderMode.FOR_DISPLAY)
page.render(tile2, clip2, transform2, TiffRenderMode.FOR_DISPLAY)
}On Android, everything above can take the platform's own ParcelFileDescriptor/Bitmap/Rect/
Matrix types directly instead of the cross-platform TiffSource/TiffBitmap/TiffRect/
TiffTransform wrappers:
// `pfd` must be a seekable ParcelFileDescriptor, e.g. from a content:// Uri via
// context.contentResolver.openFileDescriptor(uri, "r"). TiffRenderer takes ownership of it.
TiffRenderer(pfd).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
page.render(bitmap, Rect(0, 0, 512, 512), Matrix().apply { setScale(2f, 2f) })
}
}TiffTransform/the Matrix overload only ever represent an affine transform (6 components);
a genuinely non-affine (perspective) Matrix throws IllegalArgumentException, since neither
type can represent one.
import io.github.lucf15.tiffrenderer.TiffRenderer
import io.github.lucf15.tiffrenderer.TiffSource
import io.github.lucf15.tiffrenderer.TiffRenderMode
import java.io.File
TiffRenderer(TiffSource.fromFile(File("/path/to/file.tif"))).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = createTiffBitmap(page.width, page.height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY)
val pixels = bitmap.toIntArray() // packed ARGB, row-major
}
}TiffBitmap's pixels live in a direct (off-heap) ByteBuffer, so the native render call writes
straight into it with no JVM-heap copy. toIntArray() above repacks into packed ARGB ints for
convenience; bitmap.toByteArray() returns the raw RGBA8888 bytes directly (e.g. for handing to
Skia's installPixels) without that per-pixel repacking.
A direct ByteBuffer is off-heap memory. The JVM only reclaims it when its wrapper object is
garbage-collected — there's no deterministic close()/free() for it. createTiffBitmap/
TiffBitmap(width, height) allocate a fresh buffer every call. That's fine for a one-off render,
but for a large page rendered repeatedly (e.g. on every resize or scroll frame), new buffers can
pile up faster than GC reclaims the old ones and spike memory well past the heap's own -Xmx. For
that case, allocate once and reuse:
val buffer = java.nio.ByteBuffer.allocateDirect(width * height * 4)
val bitmap = TiffBitmap.wrapping(buffer, width, height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY) // render into `buffer` again on the next callTiffBitmap.wrapping requires a direct, writable buffer at least width * height * 4 bytes.
TiffRenderer per open document; close it when you're done, with close() or .use {}.TiffPage may be open at a time per TiffRenderer, matching libtiff's own
single-directory-cursor model: open, render, close before opening the next page.TiffRenderer(...), openPage(), and TiffPage#render()/retainRaster() can all throw
TiffIOException
(an unchecked RuntimeException): a directory can fail to open, or a page's compression scheme
can fail to decode, even after the document itself opened successfully — TIFF's per-directory
codec independence means decode failures are genuinely per-page, not just per-file.TiffRenderer/TiffPage instance must not be called concurrently from
more than one thread; a caller doing so (e.g. a scrolling UI rendering several pages at once)
needs its own external lock around calls into the same instance.render() decodes the entire source page into an
uncompressed RGBA raster before drawing any of it, even into a small destination bitmap — there's
no tiled or streaming decode. Use destClip/transform to control the destination size, not
to reduce how much of the source gets decoded.armeabi-v7a/x86) throw TiffIOException rather than
attempting the decode, to fail cleanly instead of risking an OOM kill.TiffSource is single-use. Each TiffSource is consumed by the TiffRenderer it's passed
to, even if construction fails; passing the same instance to a second TiffRenderer throws
IllegalStateException. If a source is created but never handed to a TiffRenderer at all, call
release() on it directly to free its underlying resource (e.g. a file descriptor) instead of
leaving that to happen whenever the object is garbage-collected.TIFFReadRGBAImageOriented, which flattens 16-bit-per-channel and floating-point TIFFs to plain
8-bit RGBA and ignores any embedded ICC profile. Scientific/medical TIFFs relying on higher
precision, or documents expecting color-managed rendering, decode without error but lose that
information silently.This is a young library, and codec support is intentionally narrow in this first version:
| Codec | Supported | Notes |
|---|---|---|
| Uncompressed | ✅ | |
| PackBits | ✅ | |
| LZW | ✅ | |
| CCITT Group 3/4 (fax) | ✅ | |
| Deflate / ZIP | ✅ | via the platform's bundled zlib |
| JPEG-in-TIFF | ✅ | via vendored IJG libjpeg |
| WebP | ✅ | via vendored libwebp |
| Zstd | ❌ | would require vendoring libzstd |
| LERC | ❌ | would require vendoring LERC |
| LZMA | ❌ | would require vendoring liblzma |
| JBIG | ❌ | untested, no fixture available |
A TIFF using an unsupported codec opens fine (the compression tag is just metadata until
something actually tries to decode pixels) but TiffPage#render() throws TiffIOException once
decoding is actually attempted. It never silently misdecodes or crashes.
The bulk of the suite lives in one shared integrationTest source set and runs, unmodified,
against the real native decode path on all three targets — Android (on-device/emulator), iOS
(simulator), and JVM — not mocked or platform-specific:
TiffRendererLifecycleTest — construction, open/close state machine, the
one-page-open-at-a-time invariant.TiffRendererCodecTest — the codec support matrix above: every supported codec must decode,
every unsupported one must throw TiffIOException specifically from render(), never from
openPage().TiffRendererRetainRasterTest — retainRaster()'s repeated-render consistency and cache
invalidation across pages.TiffRendererCorruptInputTest — hostile/corrupt TIFFs (adversarial dimensions, truncated
data) must surface as TiffIOException, never a crash.TiffRendererRenderTest — pixel-level render correctness (default fit-to-clip transform,
explicit clip, custom transform).TiffRendererMinificationTest — downscaling a fine checkerboard must blend across the mip
pyramid instead of aliasing to stark black/white.Fixtures are real .tif files under lib/core/src/commonTest/resources/, generated by
lib/core/src/commonTest/tools/generate_fixtures.py.
A handful of tests are genuinely platform-specific and live outside integrationTest:
TiffRendererAndroidNativeOverloadTest — the Bitmap/Matrix/Rect convenience overloads,
plus Android-only checks (recycled/immutable bitmaps, TiffSource.fromParcelFileDescriptor's
non-seekable-fd rejection).TiffSourceByteArrayTest / TiffBitmapOverflowTest — JVM/iOS-specific edge cases.TiffRendererConcurrencyTest / TiffRendererNativeJvmParsingTest (JVM-only) — the
per-document native lock under real thread contention, and the OS/arch native-library resolution
logic../gradlew :lib:core:jvmTest # JVM, no device/emulator needed
./gradlew :lib:core:iosSimulatorArm64Test # iOS simulator
./gradlew :lib:core:connectedAndroidDeviceTest # Android, needs a running device/emulator
Vendored as pinned git submodules under lib/native/src/main/cpp/third_party/ (the same sources
back the Android, iOS, and JVM builds):
| Library | Version |
|---|---|
| libtiff | 4.7.2 |
| IJG libjpeg | 10 |
| libwebp | 1.6.0 |
All three bump automatically, including this table (see .github/workflows/libtiff-update-check.yml,
libwebp-update-check.yml, libjpeg-update-check.yml).
Each library's copyright notice and license terms are reproduced in
THIRD_PARTY_LICENSES.md.
| Target | Platform | Architectures | Notes |
|---|---|---|---|
| Android | Android |
arm64-v8a, armeabi-v7a, x86_64, x86
|
minSdk 24+, compileSdk 37 |
| iOS | Device |
arm64 (iosArm64) |
Deployment target 13.0+ |
| iOS | Simulator |
arm64 (iosSimulatorArm64) |
No iosX64: Apple/Xcode dropped Intel simulator support |
| JVM/desktop | macOS |
aarch64, x86_64
|
JDK 17+ |
| JVM/desktop | Linux |
x86_64, aarch64
|
JDK 17+ |
| JVM/desktop | Windows |
x86_64, aarch64
|
JDK 17+ |
sample/ is a small Compose Multiplatform app that doubles as a manual test harness for every
target: pick any TIFF via the system file picker (SAF on Android, UIDocumentPickerViewController
on iOS, JFileChooser on desktop) and page through it in an edge-to-edge scrolling viewer.
sample/androidApp, sample/iosApp, and sample/desktopApp are the three platform shells;
sample/shared is the actual UI, shared between them. Run the iOS app by opening
sample/iosApp/iosApp.xcodeproj in Xcode and hitting Run; run the desktop app via
./gradlew :sample:desktopApp:run.
Apache License 2.0. See LICENSE.
A Kotlin Multiplatform library for decoding and rendering TIFF images — Android, iOS, and
JVM/desktop — including multi-page/multi-directory TIFFs, on top of libtiff.
// fd: an already-open, seekable file descriptor
TiffRenderer(TiffSource.fromFileDescriptor(fd, size)).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = createTiffBitmap(page.width, page.height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY)
}
}Android has no built-in TIFF decoder. PDF gets android.graphics.pdf.PdfRenderer, backed by
pdfium; TIFF gets nothing. TiffRenderer fills that gap. Its public API is deliberately modeled on
PdfRenderer's own shape — same method names, same lifecycle, same page/render-mode pattern — so
it's immediately familiar to any Android developer. It diverges only where the underlying reality
genuinely differs, documented inline where that happens.
TiffRenderer/TiffPage are one implementation shared across Android, iOS, and JVM — a single
platform-neutral C++ core, with only the innermost native call swapped per platform — so decoding
and rendering never drift between them. A few things worth knowing:
TiffPage. render() takes an optional destination clip and affine
transform.TiffPage#retainRaster() opts a page into decode caching, so rendering the same page at
multiple zoom levels doesn't redecode each time. Off by default — a cached raster is the page's
full uncompressed pixel grid, hundreds of MB for a large scan.TiffRenderMode.FOR_DISPLAY (bilinear + mip minification) and FOR_PRINT (nearest-neighbor)
are genuinely different resampling paths, not just labels.TiffRenderer(ParcelFileDescriptor) and
TiffPage#render(Bitmap, Rect?, Matrix?, TiffRenderMode) take the platform's own types
directly — no need to touch the cross-platform TiffSource/TiffBitmap/TiffRect/
TiffTransform wrappers at all.Versions before 2.0.0 were published to JitPack under
com.github.lucf15:TiffRenderer. That coordinate is no longer updated; migrate to the Maven Central one below.
Published to Maven Central as io.github.lucf15:tiffrenderer, a regular Kotlin Multiplatform
artifact (Android, JVM/desktop, iosArm64/iosSimulatorArm64):
// build.gradle.kts, in a Kotlin Multiplatform module
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>") // see the badge above for the latest
}
}
}A plain (non-KMP) Android app can depend on it directly instead:
// app/build.gradle.kts
dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>")
}tiffrenderer's Android target pulls in io.github.lucf15:tiffrenderer-native (the compiled
.sos) transitively; nothing extra to add for that.
Any Kotlin Multiplatform project can depend on the Maven coordinate above from its own
iosMain/iosArm64/iosSimulatorArm64 source sets — no separate iOS-specific artifact. A pure
Swift/Xcode project with no Kotlin involved still needs a real .framework/XCFramework, which
this coordinate alone doesn't provide (Maven has no such artifact shape); build one from source
the way sample/iosApp does, via :lib:core's embedAndSignAppleFrameworkForXcode Gradle task.
Same coordinate, jvm() target — works from a plain JVM project too, not just KMP:
// build.gradle.kts
dependencies {
implementation("io.github.lucf15:tiffrenderer:<version>")
}Bundles native libraries for macOS (aarch64, x86_64), Linux (x86_64, aarch64), and Windows
(x86_64, aarch64); the right one is picked automatically at runtime based on the host JVM's
os.name/os.arch.
To build against a local clone instead of the published artifact, a Gradle composite build:
// settings.gradle.kts, in the app that wants to depend on this library
includeBuild("../path/to/TiffRenderer") {
dependencySubstitution {
substitute(module("io.github.lucf15:tiffrenderer")).using(project(":lib:core"))
}
}Building the library requires the Android NDK and CMake, versions pinned in
gradle/libs.versions.toml; Gradle fetches them automatically if they aren't already installed.
Building for iOS additionally requires Xcode. :lib:core's iOS cinterop tasks depend on
:lib:native's buildTiffCoreForIos task, so the native cross-compile
(lib/native/src/main/cpp/build-ios.sh) runs automatically as part of any Gradle iOS build — no
manual step needed.
libtiff, libjpeg, and libwebp are all vendored as git submodules. Clone with
--recurse-submodules, or run git submodule update --init --recursive afterwards, before
building any platform.
The snippet at the top of this README is the minimal case: open a source, open a page, render it. A few more common cases:
Rendering into a sub-region with a custom transform:
page.render(
bitmap,
destClip = TiffRect(0, 0, 512, 512),
transform = TiffTransform(floatArrayOf(2f, 0f, 0f, 0f, 2f, 0f)), // 2x scale
renderMode = TiffRenderMode.FOR_DISPLAY,
)Rendering the same page repeatedly (e.g. multiple zoom tiles) without redecoding each time:
renderer.openPage(0).use { page ->
page.retainRaster()
// every render() call below reuses the same decode
page.render(tile1, clip1, transform1, TiffRenderMode.FOR_DISPLAY)
page.render(tile2, clip2, transform2, TiffRenderMode.FOR_DISPLAY)
}On Android, everything above can take the platform's own ParcelFileDescriptor/Bitmap/Rect/
Matrix types directly instead of the cross-platform TiffSource/TiffBitmap/TiffRect/
TiffTransform wrappers:
// `pfd` must be a seekable ParcelFileDescriptor, e.g. from a content:// Uri via
// context.contentResolver.openFileDescriptor(uri, "r"). TiffRenderer takes ownership of it.
TiffRenderer(pfd).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
page.render(bitmap, Rect(0, 0, 512, 512), Matrix().apply { setScale(2f, 2f) })
}
}TiffTransform/the Matrix overload only ever represent an affine transform (6 components);
a genuinely non-affine (perspective) Matrix throws IllegalArgumentException, since neither
type can represent one.
import io.github.lucf15.tiffrenderer.TiffRenderer
import io.github.lucf15.tiffrenderer.TiffSource
import io.github.lucf15.tiffrenderer.TiffRenderMode
import java.io.File
TiffRenderer(TiffSource.fromFile(File("/path/to/file.tif"))).use { renderer ->
renderer.openPage(0).use { page ->
val bitmap = createTiffBitmap(page.width, page.height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY)
val pixels = bitmap.toIntArray() // packed ARGB, row-major
}
}TiffBitmap's pixels live in a direct (off-heap) ByteBuffer, so the native render call writes
straight into it with no JVM-heap copy. toIntArray() above repacks into packed ARGB ints for
convenience; bitmap.toByteArray() returns the raw RGBA8888 bytes directly (e.g. for handing to
Skia's installPixels) without that per-pixel repacking.
A direct ByteBuffer is off-heap memory. The JVM only reclaims it when its wrapper object is
garbage-collected — there's no deterministic close()/free() for it. createTiffBitmap/
TiffBitmap(width, height) allocate a fresh buffer every call. That's fine for a one-off render,
but for a large page rendered repeatedly (e.g. on every resize or scroll frame), new buffers can
pile up faster than GC reclaims the old ones and spike memory well past the heap's own -Xmx. For
that case, allocate once and reuse:
val buffer = java.nio.ByteBuffer.allocateDirect(width * height * 4)
val bitmap = TiffBitmap.wrapping(buffer, width, height)
page.render(bitmap, renderMode = TiffRenderMode.FOR_DISPLAY) // render into `buffer` again on the next callTiffBitmap.wrapping requires a direct, writable buffer at least width * height * 4 bytes.
TiffRenderer per open document; close it when you're done, with close() or .use {}.TiffPage may be open at a time per TiffRenderer, matching libtiff's own
single-directory-cursor model: open, render, close before opening the next page.TiffRenderer(...), openPage(), and TiffPage#render()/retainRaster() can all throw
TiffIOException
(an unchecked RuntimeException): a directory can fail to open, or a page's compression scheme
can fail to decode, even after the document itself opened successfully — TIFF's per-directory
codec independence means decode failures are genuinely per-page, not just per-file.TiffRenderer/TiffPage instance must not be called concurrently from
more than one thread; a caller doing so (e.g. a scrolling UI rendering several pages at once)
needs its own external lock around calls into the same instance.render() decodes the entire source page into an
uncompressed RGBA raster before drawing any of it, even into a small destination bitmap — there's
no tiled or streaming decode. Use destClip/transform to control the destination size, not
to reduce how much of the source gets decoded.armeabi-v7a/x86) throw TiffIOException rather than
attempting the decode, to fail cleanly instead of risking an OOM kill.TiffSource is single-use. Each TiffSource is consumed by the TiffRenderer it's passed
to, even if construction fails; passing the same instance to a second TiffRenderer throws
IllegalStateException. If a source is created but never handed to a TiffRenderer at all, call
release() on it directly to free its underlying resource (e.g. a file descriptor) instead of
leaving that to happen whenever the object is garbage-collected.TIFFReadRGBAImageOriented, which flattens 16-bit-per-channel and floating-point TIFFs to plain
8-bit RGBA and ignores any embedded ICC profile. Scientific/medical TIFFs relying on higher
precision, or documents expecting color-managed rendering, decode without error but lose that
information silently.This is a young library, and codec support is intentionally narrow in this first version:
| Codec | Supported | Notes |
|---|---|---|
| Uncompressed | ✅ | |
| PackBits | ✅ | |
| LZW | ✅ | |
| CCITT Group 3/4 (fax) | ✅ | |
| Deflate / ZIP | ✅ | via the platform's bundled zlib |
| JPEG-in-TIFF | ✅ | via vendored IJG libjpeg |
| WebP | ✅ | via vendored libwebp |
| Zstd | ❌ | would require vendoring libzstd |
| LERC | ❌ | would require vendoring LERC |
| LZMA | ❌ | would require vendoring liblzma |
| JBIG | ❌ | untested, no fixture available |
A TIFF using an unsupported codec opens fine (the compression tag is just metadata until
something actually tries to decode pixels) but TiffPage#render() throws TiffIOException once
decoding is actually attempted. It never silently misdecodes or crashes.
The bulk of the suite lives in one shared integrationTest source set and runs, unmodified,
against the real native decode path on all three targets — Android (on-device/emulator), iOS
(simulator), and JVM — not mocked or platform-specific:
TiffRendererLifecycleTest — construction, open/close state machine, the
one-page-open-at-a-time invariant.TiffRendererCodecTest — the codec support matrix above: every supported codec must decode,
every unsupported one must throw TiffIOException specifically from render(), never from
openPage().TiffRendererRetainRasterTest — retainRaster()'s repeated-render consistency and cache
invalidation across pages.TiffRendererCorruptInputTest — hostile/corrupt TIFFs (adversarial dimensions, truncated
data) must surface as TiffIOException, never a crash.TiffRendererRenderTest — pixel-level render correctness (default fit-to-clip transform,
explicit clip, custom transform).TiffRendererMinificationTest — downscaling a fine checkerboard must blend across the mip
pyramid instead of aliasing to stark black/white.Fixtures are real .tif files under lib/core/src/commonTest/resources/, generated by
lib/core/src/commonTest/tools/generate_fixtures.py.
A handful of tests are genuinely platform-specific and live outside integrationTest:
TiffRendererAndroidNativeOverloadTest — the Bitmap/Matrix/Rect convenience overloads,
plus Android-only checks (recycled/immutable bitmaps, TiffSource.fromParcelFileDescriptor's
non-seekable-fd rejection).TiffSourceByteArrayTest / TiffBitmapOverflowTest — JVM/iOS-specific edge cases.TiffRendererConcurrencyTest / TiffRendererNativeJvmParsingTest (JVM-only) — the
per-document native lock under real thread contention, and the OS/arch native-library resolution
logic../gradlew :lib:core:jvmTest # JVM, no device/emulator needed
./gradlew :lib:core:iosSimulatorArm64Test # iOS simulator
./gradlew :lib:core:connectedAndroidDeviceTest # Android, needs a running device/emulator
Vendored as pinned git submodules under lib/native/src/main/cpp/third_party/ (the same sources
back the Android, iOS, and JVM builds):
| Library | Version |
|---|---|
| libtiff | 4.7.2 |
| IJG libjpeg | 10 |
| libwebp | 1.6.0 |
All three bump automatically, including this table (see .github/workflows/libtiff-update-check.yml,
libwebp-update-check.yml, libjpeg-update-check.yml).
Each library's copyright notice and license terms are reproduced in
THIRD_PARTY_LICENSES.md.
| Target | Platform | Architectures | Notes |
|---|---|---|---|
| Android | Android |
arm64-v8a, armeabi-v7a, x86_64, x86
|
minSdk 24+, compileSdk 37 |
| iOS | Device |
arm64 (iosArm64) |
Deployment target 13.0+ |
| iOS | Simulator |
arm64 (iosSimulatorArm64) |
No iosX64: Apple/Xcode dropped Intel simulator support |
| JVM/desktop | macOS |
aarch64, x86_64
|
JDK 17+ |
| JVM/desktop | Linux |
x86_64, aarch64
|
JDK 17+ |
| JVM/desktop | Windows |
x86_64, aarch64
|
JDK 17+ |
sample/ is a small Compose Multiplatform app that doubles as a manual test harness for every
target: pick any TIFF via the system file picker (SAF on Android, UIDocumentPickerViewController
on iOS, JFileChooser on desktop) and page through it in an edge-to-edge scrolling viewer.
sample/androidApp, sample/iosApp, and sample/desktopApp are the three platform shells;
sample/shared is the actual UI, shared between them. Run the iOS app by opening
sample/iosApp/iosApp.xcodeproj in Xcode and hitting Run; run the desktop app via
./gradlew :sample:desktopApp:run.
Apache License 2.0. See LICENSE.