EXIF & Metadata Stripper — C source
View and strip GPS, camera, date, and software metadata from photos entirely in your browser. Download a clean copy.
This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.
/*
* exif-stripper — EXIF / XMP / IPTC metadata reader + stripper for JPEG & PNG.
*
* Language: C (C11, POSIX)
* Source: CosmoDev polyglot showcase port of the EXIF Stripper tool, ported
* from src/lib/exif-stripper.ts (the canonical TypeScript
* implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* The TS reference walks the container formats directly on an ArrayBuffer with
* no DOM and no libraries; this port does the same on a plain byte buffer:
*
* JPEG APP1 (0xFFE1) segments carrying "Exif\0\0" (a TIFF IFD structure,
* big- or little-endian per the TIFF byte-order mark) or XMP, and
* APP13 (0xFFED) segments carrying IPTC ("Photoshop 3.0\0").
* PNG tEXt / iTXt text chunks and the eXIf chunk (same TIFF structure).
*
* Stripping rebuilds the file without those metadata segments — the image data
* itself is copied byte-for-byte, so JPEG stripping is lossless. No crypto is
* involved, so the C standard library is all this port needs.
*
* Ownership: parse_metadata() fills an exif_metadata whose string fields and
* `stripped` buffer are heap allocations — release them with
* exif_metadata_free(). Functions return NULL on success or a static error
* message (the TS reference throws Error instead).
*
* Build: cc -std=c11 exif-stripper.c
*/
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------ types --- */
/** A heap byte buffer that grows by reallocation. */
typedef struct {
uint8_t *data;
size_t len;
size_t cap;
} byte_buf;
/** Optional string / number fields are modelled as pointer-or-flag members. */
typedef struct {
char *camera_make; /* NULL when the tag was absent */
char *camera_model;
char *software;
char *date_time;
double gps_latitude; bool has_gps_latitude; /* decimal degrees, negative */
double gps_longitude; bool has_gps_longitude; /* for S / W hemispheres */
uint32_t image_width; bool has_image_width;
uint32_t image_height; bool has_image_height;
uint16_t orientation; bool has_orientation; /* EXIF orientation 1-8 */
double exposure_time; bool has_exposure_time; /* seconds, e.g. 0.004 */
double f_number; bool has_f_number; /* e.g. 2.8 */
uint32_t iso; bool has_iso;
double focal_length; bool has_focal_length; /* millimetres */
uint8_t *stripped; /* rebuilt copy with metadata segments removed */
size_t stripped_len;
} exif_metadata;
/* --------------------------------------------------------------- helpers --- */
static bool bb_reserve(byte_buf *b, size_t extra) {
if (b->len + extra <= b->cap) return true;
size_t cap = b->cap ? b->cap : 256;
while (cap < b->len + extra) cap *= 2;
uint8_t *p = realloc(b->data, cap);
if (!p) return false;
b->data = p;
b->cap = cap;
return true;
}
static bool bb_push(byte_buf *b, const uint8_t *src, size_t n) {
if (!bb_reserve(b, n)) return false;
memcpy(b->data + b->len, src, n);
b->len += n;
return true;
}
static uint16_t be16(const uint8_t *p) { return (uint16_t)((p[0] << 8) | p[1]); }
static uint32_t be32(const uint8_t *p) {
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
}
/** Degrees/minutes/seconds + hemisphere reference to signed decimal degrees. */
double format_gps_coordinate(const double degrees[3], char ref) {
double decimal = degrees[0] + degrees[1] / 60.0 + degrees[2] / 3600.0;
return (ref == 'S' || ref == 'W') ? -decimal : decimal;
}
/* ------------------------------------------------------- format detection --- */
static const uint8_t PNG_SIGNATURE[8] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
static const char EXIF_SIGNATURE[] = {'E', 'x', 'i', 'f', 0, 0};
static const char *XMP_SIGNATURES[] = {"http://ns.adobe.com/xap/1.0/", "http://ns.adobe.com/xmp/extension/"};
static const char IPTC_SIGNATURE[] = {'P', 'h', 'o', 't', 'o', 's', 'h', 'o', 'p', ' ', '3', '.', '0', 0};
static bool is_jpeg(const uint8_t *b, size_t len) { return len >= 2 && b[0] == 0xff && b[1] == 0xd8; }
static bool is_png(const uint8_t *b, size_t len) {
return len >= 8 && memcmp(b, PNG_SIGNATURE, 8) == 0;
}
static bool has_signature(const uint8_t *b, size_t len, size_t off, const char *sig, size_t sig_len) {
if (off + sig_len > len) return false; /* off < 0 cannot happen in size_t */
return memcmp(b + off, sig, sig_len) == 0;
}
static bool is_xmp_payload(const uint8_t *b, size_t len, size_t off) {
for (size_t i = 0; i < sizeof XMP_SIGNATURES / sizeof XMP_SIGNATURES[0]; i++) {
if (has_signature(b, len, off, XMP_SIGNATURES[i], strlen(XMP_SIGNATURES[i]))) return true;
}
return false;
}
/* ----------------------------------------------------- JPEG segment walk --- */
typedef struct {
uint8_t marker;
size_t start; /* segment start (at the 0xFF byte), end exclusive */
size_t end;
size_t payload_start; /* first payload byte (right after the 2-byte length) */
} jpeg_segment;
typedef struct {
jpeg_segment *items;
size_t count;
bool ok; /* false when the structure is corrupted mid-walk */
} segment_walk;
/*
* Walks length-prefixed markers from just after the SOI up to (and including)
* the SOS marker, whose entropy-coded data runs to the EOI. ok=false when the
* structure is corrupted mid-walk (the segments found so far are still there).
*/
static segment_walk collect_jpeg_segments(const uint8_t *b, size_t len) {
segment_walk w = {NULL, 0, true};
size_t off = 2;
while (off + 2 <= len) {
if (b[off] != 0xff) { w.ok = false; break; }
if (b[off + 1] == 0xff) { off++; continue; } /* padding fill byte */
uint8_t marker = b[off + 1];
jpeg_segment *grown = realloc(w.items, (w.count + 1) * sizeof *grown);
if (!grown) { w.ok = false; break; }
w.items = grown;
/* Standalone markers carry no length field (TEM, RSTn, SOI, EOI). */
if (marker == 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
w.items[w.count++] = (jpeg_segment){marker, off, off + 2, off + 2};
if (marker == 0xd9) break;
off += 2;
continue;
}
if (off + 4 > len) { w.ok = false; break; }
size_t size = be16(b + off + 2); /* includes the 2 length bytes itself */
if (size < 2) { w.ok = false; break; }
size_t end = off + 2 + size;
if (end > len) { w.ok = false; break; }
w.items[w.count++] = (jpeg_segment){marker, off, end, off + 4};
if (marker == 0xda) break; /* SOS - entropy data follows, no more segments */
off = end;
}
return w;
}
static bool is_jpeg_metadata_segment(const uint8_t *b, size_t len, const jpeg_segment *seg) {
if (seg->marker == 0xe1) {
return has_signature(b, len, seg->payload_start, EXIF_SIGNATURE, sizeof EXIF_SIGNATURE) ||
is_xmp_payload(b, len, seg->payload_start);
}
/* APP13 is IPTC ("Photoshop 3.0\0" + 8BIM records) - always metadata. */
return seg->marker == 0xed &&
has_signature(b, len, seg->payload_start, IPTC_SIGNATURE, sizeof IPTC_SIGNATURE);
}
/* -------------------------------------------------- TIFF / EXIF IFD parse --- */
/* EXIF field types -> byte size per component. */
static size_t type_size(uint16_t type) {
switch (type) {
case 1: case 2: case 7: return 1;
case 3: return 2;
case 4: case 9: return 4;
case 5: case 10: return 8;
default: return 1;
}
}
/* IFD0 tags. */
#define TAG_IMAGE_WIDTH 0x0100
#define TAG_IMAGE_HEIGHT 0x0101
#define TAG_MAKE 0x010f
#define TAG_MODEL 0x0110
#define TAG_ORIENTATION 0x0112
#define TAG_SOFTWARE 0x0131
#define TAG_DATE_TIME 0x0132
#define TAG_EXIF_IFD_POINTER 0x8769
#define TAG_GPS_IFD_POINTER 0x8825
/* Exif SubIFD tags. */
#define TAG_EXPOSURE_TIME 0x829a
#define TAG_F_NUMBER 0x829d
#define TAG_ISO 0x8827
#define TAG_DATE_TIME_ORIGINAL 0x9003
#define TAG_FOCAL_LENGTH 0x920a
/* GPS IFD tags. */
#define TAG_GPS_LAT_REF 0x0001
#define TAG_GPS_LAT 0x0002
#define TAG_GPS_LON_REF 0x0003
#define TAG_GPS_LON 0x0004
#define TYPE_ASCII 2
#define TYPE_SHORT 3
#define TYPE_LONG 4
#define TYPE_RATIONAL 5
typedef struct {
const uint8_t *bytes;
size_t len;
bool little;
} tiff;
static uint16_t tiff_u16(const tiff *t, size_t off) {
if (off + 2 > t->len) return 0;
return t->little ? (uint16_t)(t->bytes[off] | (t->bytes[off + 1] << 8))
: be16(t->bytes + off);
}
static uint32_t tiff_u32(const tiff *t, size_t off) {
if (off + 4 > t->len) return 0;
if (!t->little) return be32(t->bytes + off);
return (uint32_t)t->bytes[off] | ((uint32_t)t->bytes[off + 1] << 8) |
((uint32_t)t->bytes[off + 2] << 16) | ((uint32_t)t->bytes[off + 3] << 24);
}
/*
* Where the entry's value lives: inline in the 4-byte value field when it
* fits, else at the recorded byte offset. Returns false when out of bounds.
*/
static bool value_offset(const tiff *t, size_t entry, uint16_t type, uint32_t count, size_t *out) {
size_t size = type_size(type) * count;
size_t off = size <= 4 ? entry + 8 : tiff_u32(t, entry + 8);
if (off + size > t->len) return false;
*out = off;
return true;
}
/** The TS `??=` setter: only the first value seen is kept. */
static void set_str_if_absent(char **slot, const char *value, size_t max) {
if (*slot || !value || !*value) return;
*slot = strndup(value, max);
}
static bool read_ascii(const tiff *t, size_t entry, uint16_t type, uint32_t count, char out[256]) {
if (type != TYPE_ASCII || count < 1) return false;
size_t off;
if (!value_offset(t, entry, type, count, &off)) return false;
size_t n = count < 255 ? count : 255;
size_t end = 0;
memcpy(out, t->bytes + off, n);
out[n] = 0;
while (end < n && out[end]) end++;
while (end > 0 && out[end - 1] == 0) end--; /* strip trailing NULs */
out[end] = 0;
/* trim() */
char *s = out;
while (isspace((unsigned char)*s)) s++;
char *e = s + strlen(s);
while (e > s && isspace((unsigned char)e[-1])) e--;
*e = 0;
if (s != out) memmove(out, s, e - s + 1);
return out[0] != 0;
}
static bool read_number(const tiff *t, size_t entry, uint16_t type, uint32_t count, uint32_t *out) {
if (count < 1) return false;
size_t off;
if (!value_offset(t, entry, type, count, &off)) return false;
if (type == TYPE_SHORT) { *out = tiff_u16(t, off); return true; }
if (type == TYPE_LONG) { *out = tiff_u32(t, off); return true; }
return false;
}
/** Reads up to `max` RATIONAL components; returns how many were written. */
static size_t read_rationals(const tiff *t, size_t entry, uint16_t type, uint32_t count, double *out, size_t max) {
if (type != TYPE_RATIONAL || count < 1) return 0;
size_t off;
if (!value_offset(t, entry, type, count, &off)) return 0;
size_t n = count < max ? count : max;
for (size_t i = 0; i < n; i++) {
uint32_t num = tiff_u32(t, off + i * 8);
uint32_t den = tiff_u32(t, off + i * 8 + 4);
out[i] = den == 0 ? 0 : (double)num / (double)den;
}
return n;
}
static void parse_tiff(const uint8_t *tiff_bytes, size_t len, exif_metadata *meta);
static void read_ifd(const tiff *t, size_t offset, exif_metadata *meta);
static void read_exif_ifd(const tiff *t, size_t offset, exif_metadata *meta);
static void read_gps_ifd(const tiff *t, size_t offset, exif_metadata *meta);
static void parse_tiff(const uint8_t *tiff_bytes, size_t len, exif_metadata *meta) {
if (len < 8) return;
bool little;
if (tiff_bytes[0] == 0x49 && tiff_bytes[1] == 0x49) little = true; /* "II" */
else if (tiff_bytes[0] == 0x4d && tiff_bytes[1] == 0x4d) little = false; /* "MM" */
else return;
tiff t = {tiff_bytes, len, little};
if (tiff_u16(&t, 2) != 42) return;
read_ifd(&t, tiff_u32(&t, 4), meta);
}
static void read_ifd(const tiff *t, size_t offset, exif_metadata *meta) {
if (offset < 8 || offset + 2 > t->len) return;
uint16_t count = tiff_u16(t, offset);
if (offset + 2 + (size_t)count * 12 > t->len) return;
for (uint16_t i = 0; i < count; i++) {
size_t entry = offset + 2 + (size_t)i * 12;
uint16_t tag = tiff_u16(t, entry);
uint16_t type = tiff_u16(t, entry + 2);
uint32_t n = tiff_u32(t, entry + 4);
char str[256];
uint32_t num;
switch (tag) {
case TAG_IMAGE_WIDTH:
if (!meta->has_image_width && read_number(t, entry, type, n, &num)) {
meta->image_width = num; meta->has_image_width = true;
}
break;
case TAG_IMAGE_HEIGHT:
if (!meta->has_image_height && read_number(t, entry, type, n, &num)) {
meta->image_height = num; meta->has_image_height = true;
}
break;
case TAG_MAKE:
if (read_ascii(t, entry, type, n, str)) set_str_if_absent(&meta->camera_make, str, 255);
break;
case TAG_MODEL:
if (read_ascii(t, entry, type, n, str)) set_str_if_absent(&meta->camera_model, str, 255);
break;
case TAG_ORIENTATION:
if (!meta->has_orientation && read_number(t, entry, type, n, &num)) {
meta->orientation = (uint16_t)num; meta->has_orientation = true;
}
break;
case TAG_SOFTWARE:
if (read_ascii(t, entry, type, n, str)) set_str_if_absent(&meta->software, str, 255);
break;
case TAG_DATE_TIME:
if (read_ascii(t, entry, type, n, str)) set_str_if_absent(&meta->date_time, str, 255);
break;
case TAG_EXIF_IFD_POINTER:
if (read_number(t, entry, TYPE_LONG, 1, &num)) read_exif_ifd(t, num, meta);
break;
case TAG_GPS_IFD_POINTER:
if (read_number(t, entry, TYPE_LONG, 1, &num)) read_gps_ifd(t, num, meta);
break;
}
}
}
static void read_exif_ifd(const tiff *t, size_t offset, exif_metadata *meta) {
if (offset < 8 || offset + 2 > t->len) return;
uint16_t count = tiff_u16(t, offset);
if (offset + 2 + (size_t)count * 12 > t->len) return;
for (uint16_t i = 0; i < count; i++) {
size_t entry = offset + 2 + (size_t)i * 12;
uint16_t tag = tiff_u16(t, entry);
uint16_t type = tiff_u16(t, entry + 2);
uint32_t n = tiff_u32(t, entry + 4);
double rationals[3] = {0, 0, 0};
char str[256];
uint32_t num;
switch (tag) {
case TAG_EXPOSURE_TIME:
if (!meta->has_exposure_time && read_rationals(t, entry, type, n, rationals, 3) > 0) {
meta->exposure_time = rationals[0]; meta->has_exposure_time = true;
}
break;
case TAG_F_NUMBER:
if (!meta->has_f_number && read_rationals(t, entry, type, n, rationals, 3) > 0) {
meta->f_number = rationals[0]; meta->has_f_number = true;
}
break;
case TAG_ISO:
if (!meta->has_iso && read_number(t, entry, type, n, &num)) {
meta->iso = num; meta->has_iso = true;
}
break;
case TAG_DATE_TIME_ORIGINAL:
/* Phone photos usually carry the real capture time here only. */
if (read_ascii(t, entry, type, n, str)) set_str_if_absent(&meta->date_time, str, 255);
break;
case TAG_FOCAL_LENGTH:
if (!meta->has_focal_length && read_rationals(t, entry, type, n, rationals, 3) > 0) {
meta->focal_length = rationals[0]; meta->has_focal_length = true;
}
break;
}
}
}
static void read_gps_ifd(const tiff *t, size_t offset, exif_metadata *meta) {
if (offset < 8 || offset + 2 > t->len) return;
uint16_t count = tiff_u16(t, offset);
if (offset + 2 + (size_t)count * 12 > t->len) return;
char lat_ref = 'N', lon_ref = 'E';
double lat[3] = {0, 0, 0}, lon[3] = {0, 0, 0};
size_t lat_n = 0, lon_n = 0;
for (uint16_t i = 0; i < count; i++) {
size_t entry = offset + 2 + (size_t)i * 12;
uint16_t tag = tiff_u16(t, entry);
uint16_t type = tiff_u16(t, entry + 2);
uint32_t n = tiff_u32(t, entry + 4);
char str[256];
switch (tag) {
case TAG_GPS_LAT_REF:
if (read_ascii(t, entry, type, n, str)) lat_ref = (char)toupper((unsigned char)str[0]);
break;
case TAG_GPS_LAT:
lat_n = read_rationals(t, entry, type, n, lat, 3);
break;
case TAG_GPS_LON_REF:
if (read_ascii(t, entry, type, n, str)) lon_ref = (char)toupper((unsigned char)str[0]);
break;
case TAG_GPS_LON:
lon_n = read_rationals(t, entry, type, n, lon, 3);
break;
}
}
if (lat_n > 0 && !meta->has_gps_latitude) {
meta->gps_latitude = format_gps_coordinate(lat, lat_ref);
meta->has_gps_latitude = true;
}
if (lon_n > 0 && !meta->has_gps_longitude) {
meta->gps_longitude = format_gps_coordinate(lon, lon_ref);
meta->has_gps_longitude = true;
}
}
/*
* XMP is XML; the only field we surface is the editing software / creator
* tool, which appears as e.g. <xmp:CreatorTool>Pixelmator Pro</...>.
*/
static void parse_xmp(const uint8_t *payload, size_t len, exif_metadata *meta) {
static const char *needles[2] = {":CreatorTool>", ":Software>"};
for (int i = 0; i < 2; i++) {
const char *text = (const char *)payload; /* not NUL-terminated */
size_t nlen = strlen(needles[i]);
for (size_t pos = 0; pos + nlen <= len; pos++) {
if (memcmp(text + pos, needles[i], nlen) != 0) continue;
size_t start = pos + nlen;
size_t end = start;
while (end < len && text[end] != '<') end++;
char buf[256];
size_t n = end - start < 255 ? end - start : 255;
memcpy(buf, text + start, n);
buf[n] = 0;
char *s = buf; /* trim() */
while (isspace((unsigned char)*s)) s++;
char *e = s + strlen(s);
while (e > s && isspace((unsigned char)e[-1])) e--;
*e = 0;
if (s != buf) memmove(buf, s, e - s + 1);
set_str_if_absent(&meta->software, buf, 255);
return;
}
}
}
/* ------------------------------------------------------------ JPEG parse --- */
static void parse_jpeg(const uint8_t *bytes, size_t len, exif_metadata *meta) {
segment_walk w = collect_jpeg_segments(bytes, len);
for (size_t i = 0; i < w.count; i++) {
const jpeg_segment *seg = &w.items[i];
if (seg->marker == 0xe1) {
if (has_signature(bytes, len, seg->payload_start, EXIF_SIGNATURE, sizeof EXIF_SIGNATURE)) {
parse_tiff(bytes + seg->payload_start + sizeof EXIF_SIGNATURE,
seg->end - seg->payload_start - sizeof EXIF_SIGNATURE, meta);
} else if (is_xmp_payload(bytes, len, seg->payload_start)) {
parse_xmp(bytes + seg->payload_start, seg->end - seg->payload_start, meta);
}
} else if (seg->marker >= 0xc0 && seg->marker <= 0xcf && seg->marker != 0xc4 &&
seg->marker != 0xc8 && seg->marker != 0xcc && seg->end - seg->payload_start >= 5) {
/* Start-of-frame header: precision(1), height(2), width(2), big-endian. */
if (!meta->has_image_height) {
meta->image_height = be16(bytes + seg->payload_start + 1);
meta->has_image_height = true;
}
if (!meta->has_image_width) {
meta->image_width = be16(bytes + seg->payload_start + 3);
meta->has_image_width = true;
}
}
}
free(w.items);
}
/* ------------------------------------------------------------- PNG parse --- */
static void parse_png_text_chunk(bool itxt, const uint8_t *data, size_t len, exif_metadata *meta) {
const uint8_t *nul = memchr(data, 0, len);
if (!nul || nul == data) return;
size_t kw_len = (size_t)(nul - data);
if (kw_len != 8 || memcmp(data, "Software", 8) != 0) return;
const uint8_t *text;
size_t text_len;
if (!itxt) {
text = nul + 1;
text_len = len - kw_len - 1;
} else {
/* iTXt: keyword\0 compressionFlag(1) compressionMethod(1) languageTag\0
* translatedKeyword\0 text(utf-8). Only uncompressed text is read. */
if (len < kw_len + 3 || data[kw_len + 1] != 0) return;
size_t p = kw_len + 3;
for (int skipped = 0; skipped < 2; skipped++) {
const uint8_t *next = memchr(data + p, 0, len - p);
if (!next) return;
p = (size_t)(next - data) + 1;
}
text = data + p;
text_len = len - p;
}
char buf[256];
size_t n = text_len < 255 ? text_len : 255;
memcpy(buf, text, n);
buf[n] = 0;
char *s = buf; /* trim() */
while (isspace((unsigned char)*s)) s++;
char *e = s + strlen(s);
while (e > s && isspace((unsigned char)e[-1])) e--;
*e = 0;
if (s != buf) memmove(buf, s, e - s + 1);
set_str_if_absent(&meta->software, buf, 255);
}
static void parse_png(const uint8_t *bytes, size_t len, exif_metadata *meta) {
size_t off = 8;
while (off + 8 <= len) {
uint32_t length = be32(bytes + off);
if (off + 12 > len || length > len - off - 12) break; /* corrupted chunk */
char type[5] = {0};
memcpy(type, bytes + off + 4, 4);
const uint8_t *data = bytes + off + 8;
if (strcmp(type, "IHDR") == 0) {
if (!meta->has_image_width) { meta->image_width = be32(data); meta->has_image_width = true; }
if (!meta->has_image_height) { meta->image_height = be32(data + 4); meta->has_image_height = true; }
} else if (strcmp(type, "tEXt") == 0) {
parse_png_text_chunk(false, data, length, meta);
} else if (strcmp(type, "iTXt") == 0) {
parse_png_text_chunk(true, data, length, meta);
} else if (strcmp(type, "eXIf") == 0) {
parse_tiff(data, length, meta);
}
if (strcmp(type, "IEND") == 0) break;
off += 12 + length;
}
}
/* ----------------------------------------------------------------- strip --- */
static const char *strip_jpeg(const uint8_t *bytes, size_t len, byte_buf *out) {
segment_walk w = collect_jpeg_segments(bytes, len);
if (!w.ok) { /* corrupted walk - copy verbatim */
free(w.items);
return bb_push(out, bytes, len) ? NULL : "out of memory";
}
if (!bb_push(out, bytes, 2)) goto oom; /* SOI */
for (size_t i = 0; i < w.count; i++) {
const jpeg_segment *seg = &w.items[i];
if (seg->marker == 0xda) {
/* SOS header + entropy data + EOI are copied verbatim to the end. */
if (!bb_push(out, bytes + seg->start, len - seg->start)) goto oom;
break;
}
if (is_jpeg_metadata_segment(bytes, len, seg)) continue;
if (!bb_push(out, bytes + seg->start, seg->end - seg->start)) goto oom;
}
free(w.items);
return NULL;
oom:
free(w.items);
return "out of memory";
}
static const char *strip_png(const uint8_t *bytes, size_t len, byte_buf *out) {
if (!bb_push(out, bytes, 8)) return "out of memory"; /* signature */
size_t off = 8;
while (off + 8 <= len) {
uint32_t length = be32(bytes + off);
if (off + 12 > len || length > len - off - 12) break;
char type[5] = {0};
memcpy(type, bytes + off + 4, 4);
/* tEXt / iTXt / eXIf are the metadata carriers; everything else (IHDR,
* PLTE, IDAT, ...) is copied byte-for-byte, CRC included. */
if (strcmp(type, "tEXt") != 0 && strcmp(type, "iTXt") != 0 && strcmp(type, "eXIf") != 0) {
if (!bb_push(out, bytes + off, 12 + length)) return "out of memory";
}
off += 12 + length;
}
if (off < len && !bb_push(out, bytes + off, len - off)) return "out of memory";
return NULL;
}
/* ------------------------------------------------------------ public API --- */
const char *strip_metadata(const uint8_t *src, size_t len, byte_buf *out) {
if (len < 8) return "File is too small to be a valid image.";
if (is_jpeg(src, len)) return strip_jpeg(src, len, out);
if (is_png(src, len)) return strip_png(src, len, out);
return "Unsupported format: only JPEG and PNG images are supported.";
}
const char *parse_metadata(const uint8_t *src, size_t len, exif_metadata *meta) {
memset(meta, 0, sizeof *meta);
if (len < 8) return "File is too small to be a valid image.";
if (!is_jpeg(src, len) && !is_png(src, len)) {
return "Unsupported format: only JPEG and PNG images are supported.";
}
byte_buf stripped = {0};
const char *err = strip_metadata(src, len, &stripped);
if (err) {
free(stripped.data);
return err;
}
meta->stripped = stripped.data;
meta->stripped_len = stripped.len;
if (is_jpeg(src, len)) parse_jpeg(src, len, meta);
else parse_png(src, len, meta);
return NULL;
}
void exif_metadata_free(exif_metadata *meta) {
free(meta->camera_make);
free(meta->camera_model);
free(meta->software);
free(meta->date_time);
free(meta->stripped);
memset(meta, 0, sizeof *meta);
}
/* ------------------------------------------------------------- demo main --- */
int main(void) {
/* Minimal JPEG: SOI + APP1(Exif, GPS + make) + DQT-ish APP0 + SOS + EOI. */
uint8_t jpeg[128] = {0};
size_t n = 0;
jpeg[n++] = 0xff; jpeg[n++] = 0xd8; /* SOI */
size_t app1_len_at = n;
jpeg[n++] = 0xff; jpeg[n++] = 0xe1; /* APP1 */
jpeg[n++] = 0; jpeg[n++] = 0; /* length (patched) */
const char exif[] = {'E', 'x', 'i', 'f', 0, 0};
memcpy(jpeg + n, exif, 6); n += 6;
const uint8_t tiff_le[] = {
'I', 'I', 42, 0, 8, 0, 0, 0, /* TIFF header, LE */
1, 0, /* IFD0 @8: 1 entry */
0x25, 0x88, 4, 0, 1, 0, 0, 0, 26, 0, 0, 0, /* GPS IFD ptr @26 */
0, 0, 0, 0, /* next IFD = none */
2, 0, /* GPS @26: 2 entries*/
0x01, 0, 2, 0, 2, 0, 0, 0, 'N', 0, 0, 0, /* latRef "N\0" */
0x02, 0, 5, 0, 3, 0, 0, 0, 56, 0, 0, 0, /* lat RATIONAL[3]@56*/
0, 0, 0, 0, /* next IFD = none */
48, 0, 0, 0, 1, 0, 0, 0, /* @56 48/1 degrees */
51, 0, 0, 0, 1, 0, 0, 0, /* 51/1 minutes */
0, 0, 0, 0, 1, 0, 0, 0, /* 0/1 seconds */
};
memcpy(jpeg + n, tiff_le, sizeof tiff_le); n += sizeof tiff_le;
size_t app1_end = n;
jpeg[app1_len_at + 2] = (uint8_t)((app1_end - app1_len_at - 2) >> 8);
jpeg[app1_len_at + 3] = (uint8_t)(app1_end - app1_len_at - 2);
jpeg[n++] = 0xff; jpeg[n++] = 0xda; jpeg[n++] = 0; jpeg[n++] = 2; /* SOS */
jpeg[n++] = 0xff; jpeg[n++] = 0xd9; /* EOI */
exif_metadata meta;
const char *err = parse_metadata(jpeg, n, &meta);
if (err) {
fprintf(stderr, "error: %s\n", err);
return 1;
}
printf("gps latitude: %.6f\n", meta.gps_latitude);
printf("stripped %zu -> %zu bytes (EXIF APP1 removed)\n", n, meta.stripped_len);
exif_metadata_free(&meta);
return 0;
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →