C library for reading and writing image metadata. Runs exiftool inside a WebAssembly sandbox via WAMR in AOT mode.
AGPL-3.0. See LICENSE.
Requires CMake 3.14+ and a C23 compiler.
git submodule update --init
cmake -B build
cmake --build build
Produces libexif.a, a static library with WAMR baked in.
The AOT-compiled exiftool module must exist at resources/zeroperl.aot. Generate it with:
./scripts/update-aot.sh <path/to/zeroperl.wasm>
This requires LLVM 18: brew install llvm@18.
#include "libexif.h"exif_t *ctx = exif_create(NULL);
// ... use ctx ...
exif_destroy(ctx);exif_create loads the AOT module and initializes the WASM runtime. Reuse a single context to amortize startup cost, ~30ms.
exif_result_t r = exif_read(ctx, "/path/to/photo.jpg", NULL);
if (r.success)
printf("%.*s\n", (int)r.data_len, r.data);
exif_result_free(ctx, &r);Reads always return structured JSON with default flags -json -a -s -n -G1 -b.
From an in-memory buffer:
exif_buf_t buf = { .data = bytes, .len = nbytes, .filename = "photo.jpg" };
exif_result_t r = exif_read_buf(ctx, buf, NULL);The filename extension determines format handling.
const char *tags[] = { "-Artist=Jane", "-Comment=test" };
exif_options_t opts = { .tags = tags, .ntags = 2 };
exif_result_t r = exif_write(ctx, "/path/to/photo.jpg", "/path/to/output.jpg", &opts);
exif_result_free(ctx, &r);Pass NULL as out_path to overwrite the source file.
Buffer variant returns the modified file bytes:
exif_result_t r = exif_write_buf(ctx, buf, &opts);
// r.data contains the modified fileExtra exiftool CLI args can be passed per-call:
const char *args[] = { "-api", "geolocation" };
exif_options_t opts = { .args = args, .argc = 2 };
exif_result_t r = exif_read(ctx, path, &opts);A transform callback can post-process stdout before it's returned:
char *my_transform(const char *data, size_t len, void *ctx) {
// return a caller-owned string; the library frees the original
}
exif_options_t opts = { .transform = my_transform };exif_config_t cfg = {
.allocator = &my_allocator,
.wasm_stack_size = 8 << 20, // 8 MiB, the default
.wasm_heap_size = 32 << 20, // 32 MiB, the default
.exec_stack_size = 8 << 20, // 8 MiB, the default
};
exif_t *ctx = exif_create(&cfg);A single exif_t context is not thread-safe. Use one context per thread, or synchronize externally.
The Exif Swift package wraps the C API. Thread-safe via internal Mutex.
let exif = try Exif()
// Read from file or in-memory buffer
let json = try exif.read(from: photoURL)
let json2 = try exif.read(data: imageData, url: photoURL)
// Write to file
try exif.write(to: photoURL, tags: ["-Artist=Jane"])
try exif.write(to: photoURL, outputURL: outputURL, tags: ["-Artist=Jane"])
// Write to buffer
let modified = try exif.write(data: imageData, url: photoURL, tags: ["-Artist=Jane"])Swift tests link against libexif.a from build/:
cmake -B build && cmake --build build
swift test
./build/exif_test
swift test
./build/exif_bench <image>
Apple M-series, AOT mode:
| Operation | Time |
|---|---|
| create | ~30ms |
| read | ~60ms |
| write | ~200ms |