Skip to content

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 →