Kim - Kotlin Image Metadata

August 14, 2026 ยท View on GitHub

Kotlin JVM Android iOS Windows Linux macOS JS WASM

Kim is a Kotlin Multiplatform library for reading and writing image metadata.

Features

  • JPG: Read & Write EXIF, IPTC & XMP
  • PNG: Read & Write eXIf chunk & XMP
    • Also read non-standard EXIF & IPTC from tEXt/zTXt chunk
  • WebP: Read & Write EXIF & XMP
  • HEIC / AVIF: Read EXIF & XMP
    • Support for animated AVIF files (AV1 Image Sequence)
  • JXL: Read & Write EXIF & XMP of uncompressed files
  • TIFF / RAW: Read EXIF & XMP
    • Full support for Adobe DNG, Canon CR2, Canon CR3 & Fujifilm RAF
    • Support for Nikon NEF, Sony ARW & Panasonic RW2
    • API for preview image extraction of DNG, CR2, CR3, RAF, NEF, ARW, RW2 & ORF
  • MakerNote reading for Canon, Nikon, Sony, Fujifilm, Apple, Olympus, Panasonic, Pentax, Ricoh, Samsung, Sigma and Leica cameras, including the sub-directories with camera settings, white balance, AF, flash and lens data, the camera-encrypted Nikon data and the model specific Canon CameraInfo and CustomFunctions records
  • GIF: Read & Write XMP
  • Handling of XMP content through XMP Core for Kotlin Multiplatform
  • Convenient Kim.update() API to perform updates to the relevant places
    • JPG: Lossless rotation by modifying only one byte (where present)
  • Kim.deleteMetadata() API to remove all metadata, keeping the ICC profile

Installation

implementation("de.stefan-oltmann:kim:<version>")

For the targets wasmJs & js you also need to specify this:

implementation(npm("pako", "2.1.0"))

Sample usages

Read metadata

Kim.readMetadata() takes kotlin.ByteArray on all platforms and depending on the platform also kotlinx.io.files.Path, kotlinx.io.Source (for usage with Ktor) & ByteReadChannel, java.io.File, java.io.InputStream, NSData (iOS) and String paths.

val bytes: ByteArray = loadBytes()

val metadata = Kim.readMetadata(bytes)

/* MediaMetadata has a proper toString() similar to the output of ExifTool */
println(metadata)

val orientation = metadata.findShortValue(TiffTag.TIFF_TAG_ORIENTATION)

println("Orientation: $orientation")

val takenDate = metadata.findStringValue(ExifTag.EXIF_TAG_DATE_TIME_ORIGINAL)

println("Taken date: $takenDate")

For streaming sources, Kim.readMetadata() also takes a ByteReader, so the file does not have to be loaded into memory:

val byteReader = JvmInputStreamByteReader(inputFile.inputStream(), inputFile.length())

val metadata = Kim.readMetadata(byteReader)

Create high level summary object

This creates an instance of MetadataSummary. It contains the following:

  • Image size
  • Orientation
  • Date taken
  • GPS coordinates
  • Camera make & model
  • Lens make & model
  • ISO, Exposure time, F-Number, Focal length
  • Image title & description
  • Rating
  • XMP:pick flag
  • Keywords
  • Faces (XMP-mwg-rs regions, used by Picasa and others)
  • Persons in image
val bytes: ByteArray = loadBytes()

val summary = Kim.readMetadata(bytes).convertToSummary()

Extract metadata bytes

Kim.extractMetadataBytes() determines the file type from the file header and returns the raw metadata bytes. Cloud services can not reliably tell the mime type, so this can be used to upload the metadata alongside the image.

val result = Kim.extractMetadataBytes(byteReader)

/* The detected media format, or NULL when it could not be determined. */
val mediaFormat: MediaFormat? = result.first

/* The raw metadata bytes to upload to the cloud service. */
val metadataBytes: ByteArray = result.second

Extract preview image

Kim.extractPreviewImage() extracts the embedded preview image of DNG, CR2, CR3, RAF, NEF, ARW & RW2 files as JPEG bytes.

val previewBytes: ByteArray? = Kim.extractPreviewImage(byteReader)

if (previewBytes != null)
    println("Preview image has ${previewBytes.size} bytes.")

Change orientation using low level API

val inputFile = File("myphoto.jpg")
val outputFile = File("myphoto_changed.jpg")

val metadata = Kim.readMetadata(inputFile)

val outputSet: TiffOutputSet = metadata.exif?.createOutputSet() ?: TiffOutputSet()

val rootDirectory = outputSet.getOrCreateRootDirectory()

rootDirectory.removeField(TiffTag.TIFF_TAG_ORIENTATION)
rootDirectory.add(TiffTag.TIFF_TAG_ORIENTATION, 8)

OutputStreamByteWriter(outputFile.outputStream()).use { outputStreamByteWriter ->

    JpegRewriter.updateExifMetadata(
        byteReader = JvmInputStreamByteReader(inputFile.inputStream(), inputFile.length()),
        byteWriter = outputStreamByteWriter,
        outputSet = outputSet
    )
}

See the example project for more details.

Update metadata using Kim.update () API

Kim.update() applies the given updates to all formats that can represent them, so EXIF, IPTC and XMP are updated simultaneously in one call.

val bytes: ByteArray = loadBytes()

/* A single update: */
val rotatedBytes = Kim.update(
    bytes = bytes,
    update = MetadataUpdate.Orientation(TiffOrientation.ROTATE_RIGHT)
)

/* Multiple updates in one call: */
val updatedBytes = Kim.update(
    bytes = bytes,
    updates = setOf(
        MetadataUpdate.Orientation(TiffOrientation.ROTATE_RIGHT),
        MetadataUpdate.TakenDate(timestamp),
        MetadataUpdate.Title("My title"),
        MetadataUpdate.Keywords(setOf("hello", "test"))
    )
)

The supported update types are:

UpdateSets
MetadataUpdate.OrientationRotation (JPG supports a lossless single-byte swap)
MetadataUpdate.TakenDateDate taken
MetadataUpdate.GpsCoordinatesGPS coordinates
MetadataUpdate.LocationShownLocation shown
MetadataUpdate.GpsCoordinatesAndLocationShownGPS coordinates and location
MetadataUpdate.TitleTitle
MetadataUpdate.DescriptionDescription
MetadataUpdate.FlaggedThe XMP:pick flag
MetadataUpdate.RatingStar rating
MetadataUpdate.KeywordsKeywords
MetadataUpdate.FacesFaces (XMP-mwg-rs regions)
MetadataUpdate.PersonsPersons in image

An update call without any updates is rejected with an ImageWriteException.

See AbstractUpdaterTest for more samples.

Streaming update

The update can stream the file from a ByteReader to a ByteWriter. The image data of JPEG, PNG, GIF and JPEG XL files with split codestream boxes (jxlp) is streamed in bounded chunks. WebP files buffer their chunks in memory, and JPEG XL files with a single codestream box (jxlc) buffer the codestream, because the metadata is stored behind the image data. A single-update overload exists for both the byte array and the streaming variant.

val byteReader = JvmInputStreamByteReader(inputFile.inputStream(), inputFile.length())

OutputStreamByteWriter(outputFile.outputStream()).use { outputStreamByteWriter ->

    Kim.update(
        byteReader = byteReader,
        byteWriter = outputStreamByteWriter,
        updates = setOf(MetadataUpdate.Orientation(TiffOrientation.ROTATE_RIGHT))
    )
}

Delete metadata using Kim.deleteMetadata () API

Kim.deleteMetadata() removes all metadata of a file, but keeps the ICC chunks, because they would change how the image is displayed.

  • JPG: removes EXIF, XMP, IPTC & comment segments
  • PNG: removes the eXIf chunk, all text chunks & the tIME chunk
  • WebP: removes EXIF & XMP chunks and clears the VP8X metadata flags
  • JXL: removes Exif & xml boxes
  • GIF: removes the XMP application extension & comment extensions
val bytes: ByteArray = loadBytes()

val newBytes = Kim.deleteMetadata(bytes)

Like Kim.update(), deleteMetadata() also offers a streaming overload that writes to a ByteWriter without loading the file into memory for the formats listed in the Streaming update section:

Kim.deleteMetadata(
    byteReader = byteReader,
    byteWriter = byteWriter
)

Update thumbnail using Kim.updateThumbnail () API

val bytes: ByteArray = loadBytes()
val thumbnailBytes: ByteArray = loadThumbnailBytes()

val newBytes = Kim.updateThumbnail(
    bytes = bytes,
    thumbnailBytes = thumbnailBytes
)

Using Java

See the Java example project how to use Kim in Java projects.

Limitations

  • Does not read the image size and orientation for HEIC, AVIF & JPEG XL.
  • Updates buffer the file content in memory for WebP files and for JPEG XL files with a single codestream box (jxlc). JPEG, PNG, GIF and JPEG XL files with split codestream boxes (jxlp) are streamed in bounded chunks.
  • Does not read brotli compressed metadata of JPEG XL due to missing brotli KMP libs.
  • The MakerNotes of GoPro cameras and the undocumented records of the oldest Canon and Sony models are not interpreted.
  • There is right now no convienient tooling for GeoTiff like there is for GPS.

Regarding HEIC & AVIF metadata

In the processing of HEIC and AVIF files, we handle them as standard ISOBMFF-based files, adhering rigorously to the EIC/ISO 14496-12 specification. To preempt potential legal issues, we intentionally omit certain boxes outlined in the HEIC specification, notably the image size ("ispe") and image rotation ("irot") boxes. This approach extends to AVIF images, as they repurpose the same boxes.

Contributions

Contributions to Kim are welcome! If you encounter any issues, have suggestions for improvements, or would like to contribute new features, please feel free to submit a pull request.

Acknowledgements

License

This code is under the Apache License 2.0.

See the NOTICE.txt file for required notices and attributions.