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++ (C++17, standard library only)
// Ported from src/lib/exif-stripper.ts (the canonical TypeScript
// implementation). display source — part of CosmoDev's polyglot tool pages.
//
// Pure buffer logic - no DOM, no external libraries. The binary formats are
// parsed directly:
// - 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.
#include <cstdint>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>
namespace exif_stripper {
using Bytes = std::vector<uint8_t>;
/** Everything the parser surfaced, plus the stripped rebuild of the input. */
struct ExifMetadata {
std::optional<std::string> cameraMake;
std::optional<std::string> cameraModel;
std::optional<std::string> software;
std::optional<std::string> dateTime;
/** Decimal degrees; negative for southern / western hemispheres. */
std::optional<double> gpsLatitude;
std::optional<double> gpsLongitude;
std::optional<uint32_t> imageWidth;
std::optional<uint32_t> imageHeight;
/** EXIF orientation value 1-8. */
std::optional<uint16_t> orientation;
/** Seconds (e.g. 0.004 = 1/250 s). */
std::optional<double> exposureTime;
/** F-number (e.g. 2.8). */
std::optional<double> fNumber;
std::optional<uint32_t> iso;
/** Millimetres. */
std::optional<double> focalLength;
/** A rebuilt copy of the input with every metadata segment removed. */
Bytes stripped;
};
/** Degrees/minutes/seconds + hemisphere reference to signed decimal degrees. */
double formatGpsCoordinate(const std::vector<double>& degrees, const std::string& ref) {
const double deg = degrees.size() > 0 ? degrees[0] : 0;
const double min = degrees.size() > 1 ? degrees[1] : 0;
const double sec = degrees.size() > 2 ? degrees[2] : 0;
const double decimal = deg + min / 60 + sec / 3600;
return ref == "S" || ref == "W" ? -decimal : decimal;
}
// --- format detection ---------------------------------------------------------
static const char EXIF_SIGNATURE[] = {'E', 'x', 'i', 'f', 0, 0};
static const char* const 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 isJpeg(const Bytes& bytes) {
return bytes.size() >= 2 && bytes[0] == 0xff && bytes[1] == 0xd8;
}
static bool isPng(const Bytes& bytes) {
static const uint8_t SIG[8] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a};
return bytes.size() >= 8 &&
std::equal(SIG, SIG + 8, bytes.begin());
}
/** True when `bytes[off..]` starts with the NUL-terminated string `sig`. */
static bool hasSignature(const Bytes& bytes, std::size_t off, const char* sig) {
for (std::size_t i = 0; sig[i] != '\0' || i == 0; i++) {
if (off + i >= bytes.size() || bytes[off + i] != uint8_t(sig[i])) return false;
if (sig[i] == '\0') return true; // matched through the embedded NUL
}
return true;
}
/** Bytes [off, off+len) as a trimmed string (trailing NULs stripped). */
static std::string utf8At(const Bytes& bytes, std::size_t off, std::size_t len) {
std::size_t end = off + len;
if (end > bytes.size()) end = bytes.size();
std::string s(bytes.begin() + off, bytes.begin() + end);
while (!s.empty() && (s.back() == '\0' || s.back() == ' ' || s.back() == '\t' ||
s.back() == '\r' || s.back() == '\n')) {
s.pop_back();
}
return s;
}
// --- JPEG segment walk --------------------------------------------------------
struct JpegSegment {
uint8_t marker;
/** Segment start (at the 0xFF byte), end exclusive. */
std::size_t start;
std::size_t end;
/** First payload byte (right after the 2-byte length field). */
std::size_t payloadStart;
};
static uint16_t u16be(const Bytes& b, std::size_t off) {
return uint16_t(b[off]) << 8 | b[off + 1];
}
static uint32_t u32be(const Bytes& b, std::size_t off) {
return uint32_t(b[off]) << 24 | uint32_t(b[off + 1]) << 16 |
uint32_t(b[off + 2]) << 8 | uint32_t(b[off + 3]);
}
// Walks length-prefixed markers from just after the SOI up to (and including)
// the SOS marker, whose entropy-coded data runs to the EOI. Returns an empty
// optional when the structure is corrupted mid-walk.
static std::optional<std::vector<JpegSegment>> collectJpegSegments(const Bytes& bytes) {
std::vector<JpegSegment> segments;
std::size_t off = 2;
while (off + 2 <= bytes.size()) {
if (bytes[off] != 0xff) return std::nullopt;
if (bytes[off + 1] == 0xff) {
off++; // padding fill byte before the real marker
continue;
}
const uint8_t marker = bytes[off + 1];
// Standalone markers carry no length field (TEM, RSTn, SOI, EOI).
if (marker == 0x01 || (marker >= 0xd0 && marker <= 0xd9)) {
segments.push_back({marker, off, off + 2, off + 2});
if (marker == 0xd9) break;
off += 2;
continue;
}
if (off + 4 > bytes.size()) return std::nullopt;
const uint16_t size = u16be(bytes, off + 2); // includes the 2 length bytes
if (size < 2) return std::nullopt;
const std::size_t end = off + 2 + size;
if (end > bytes.size()) return std::nullopt;
segments.push_back({marker, off, end, off + 4});
if (marker == 0xda) break; // SOS - entropy data follows, no more segments
off = end;
}
return segments;
}
static bool isJpegMetadataSegment(const Bytes& bytes, const JpegSegment& seg) {
if (seg.marker == 0xe1) {
if (hasSignature(bytes, seg.payloadStart, EXIF_SIGNATURE)) return true;
for (const char* sig : XMP_SIGNATURES) {
if (hasSignature(bytes, seg.payloadStart, sig)) return true;
}
return false;
}
// APP13 is IPTC ("Photoshop 3.0\0" + 8BIM records) - always metadata.
return seg.marker == 0xed && hasSignature(bytes, seg.payloadStart, IPTC_SIGNATURE);
}
// --- TIFF / EXIF IFD parsing --------------------------------------------------
// EXIF field types -> byte size per component.
static std::size_t typeSize(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.
constexpr uint16_t TAG_IMAGE_WIDTH = 0x0100;
constexpr uint16_t TAG_IMAGE_HEIGHT = 0x0101;
constexpr uint16_t TAG_MAKE = 0x010f;
constexpr uint16_t TAG_MODEL = 0x0110;
constexpr uint16_t TAG_ORIENTATION = 0x0112;
constexpr uint16_t TAG_SOFTWARE = 0x0131;
constexpr uint16_t TAG_DATE_TIME = 0x0132;
constexpr uint16_t TAG_EXIF_IFD_POINTER = 0x8769;
constexpr uint16_t TAG_GPS_IFD_POINTER = 0x8825;
// Exif SubIFD tags.
constexpr uint16_t TAG_EXPOSURE_TIME = 0x829a;
constexpr uint16_t TAG_F_NUMBER = 0x829d;
constexpr uint16_t TAG_ISO = 0x8827;
constexpr uint16_t TAG_DATE_TIME_ORIGINAL = 0x9003;
constexpr uint16_t TAG_FOCAL_LENGTH = 0x920a;
// GPS IFD tags.
constexpr uint16_t TAG_GPS_LAT_REF = 0x0001;
constexpr uint16_t TAG_GPS_LAT = 0x0002;
constexpr uint16_t TAG_GPS_LON_REF = 0x0003;
constexpr uint16_t TAG_GPS_LON = 0x0004;
constexpr uint16_t TYPE_ASCII = 2;
constexpr uint16_t TYPE_SHORT = 3;
constexpr uint16_t TYPE_LONG = 4;
constexpr uint16_t TYPE_RATIONAL = 5;
struct Tiff {
const Bytes& bytes;
bool little;
uint16_t u16(std::size_t off) const {
return little ? uint16_t(bytes[off]) | uint16_t(bytes[off + 1]) << 8
: uint16_t(bytes[off]) << 8 | bytes[off + 1];
}
uint32_t u32(std::size_t off) const {
return little ? uint32_t(bytes[off]) | uint32_t(bytes[off + 1]) << 8 |
uint32_t(bytes[off + 2]) << 16 | uint32_t(bytes[off + 3]) << 24
: uint32_t(bytes[off]) << 24 | uint32_t(bytes[off + 1]) << 16 |
uint32_t(bytes[off + 2]) << 8 | uint32_t(bytes[off + 3]);
}
};
// Where the entry's value lives: inline in the 4-byte value field when it
// fits, else at the recorded byte offset. Returns nullopt when out of bounds.
static std::optional<std::size_t> valueOffset(const Tiff& t, std::size_t entry,
uint16_t type, uint32_t count) {
const std::size_t size = typeSize(type) * count;
const std::size_t off = size <= 4 ? entry + 8 : t.u32(entry + 8);
if (off + size > t.bytes.size()) return std::nullopt;
return off;
}
static std::optional<std::string> readAscii(const Tiff& t, std::size_t entry,
uint16_t type, uint32_t count) {
if (type != TYPE_ASCII || count < 1) return std::nullopt;
const auto off = valueOffset(t, entry, type, count);
if (!off) return std::nullopt;
std::string str = utf8At(t.bytes, *off, count);
// Trim trailing NULs, then surrounding whitespace; empty means absent.
std::size_t b = 0, e = str.size();
while (b < e && (str[b] == ' ' || str[b] == '\t')) b++;
std::string trimmed = str.substr(b);
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t')) {
trimmed.pop_back();
}
if (trimmed.empty()) return std::nullopt;
return trimmed;
}
static std::optional<uint32_t> readNumber(const Tiff& t, std::size_t entry,
uint16_t type, uint32_t count) {
if (count < 1) return std::nullopt;
const auto off = valueOffset(t, entry, type, count);
if (!off) return std::nullopt;
if (type == TYPE_SHORT) return uint32_t(t.u16(*off));
if (type == TYPE_LONG) return t.u32(*off);
return std::nullopt;
}
static std::optional<std::vector<double>> readRationals(const Tiff& t, std::size_t entry,
uint16_t type, uint32_t count) {
if (type != TYPE_RATIONAL || count < 1) return std::nullopt;
const auto off = valueOffset(t, entry, type, count);
if (!off) return std::nullopt;
std::vector<double> values;
for (uint32_t i = 0; i < count; i++) {
const uint32_t num = t.u32(*off + i * 8);
const uint32_t den = t.u32(*off + i * 8 + 4);
values.push_back(den == 0 ? 0 : double(num) / double(den));
}
return values;
}
static void readIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta);
static void readExifIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta);
static void readGpsIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta);
static void parseTiff(const Bytes& tiffBytes, ExifMetadata& meta) {
if (tiffBytes.size() < 8) return;
bool little;
if (tiffBytes[0] == 0x49 && tiffBytes[1] == 0x49) little = true; // "II"
else if (tiffBytes[0] == 0x4d && tiffBytes[1] == 0x4d) little = false; // "MM"
else return;
const Tiff t{tiffBytes, little};
if (t.u16(2) != 42) return;
readIfd(t, t.u32(4), meta);
}
static void readIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta) {
if (offset < 8 || offset + 2 > t.bytes.size()) return;
const uint16_t count = t.u16(offset);
const std::size_t end = offset + 2 + std::size_t(count) * 12;
if (end > t.bytes.size()) return;
for (uint16_t i = 0; i < count; i++) {
const std::size_t entry = offset + 2 + std::size_t(i) * 12;
const uint16_t tag = t.u16(entry);
const uint16_t type = t.u16(entry + 2);
const uint32_t n = t.u32(entry + 4);
switch (tag) {
case TAG_IMAGE_WIDTH:
if (!meta.imageWidth) meta.imageWidth = readNumber(t, entry, type, n);
break;
case TAG_IMAGE_HEIGHT:
if (!meta.imageHeight) meta.imageHeight = readNumber(t, entry, type, n);
break;
case TAG_MAKE:
if (!meta.cameraMake) meta.cameraMake = readAscii(t, entry, type, n);
break;
case TAG_MODEL:
if (!meta.cameraModel) meta.cameraModel = readAscii(t, entry, type, n);
break;
case TAG_ORIENTATION:
if (!meta.orientation) meta.orientation = uint16_t(readNumber(t, entry, type, n).value_or(0));
break;
case TAG_SOFTWARE:
if (!meta.software) meta.software = readAscii(t, entry, type, n);
break;
case TAG_DATE_TIME:
if (!meta.dateTime) meta.dateTime = readAscii(t, entry, type, n);
break;
case TAG_EXIF_IFD_POINTER: {
const auto sub = readNumber(t, entry, TYPE_LONG, 1);
if (sub) readExifIfd(t, *sub, meta);
break;
}
case TAG_GPS_IFD_POINTER: {
const auto gps = readNumber(t, entry, TYPE_LONG, 1);
if (gps) readGpsIfd(t, *gps, meta);
break;
}
default: break;
}
}
}
static void readExifIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta) {
if (offset < 8 || offset + 2 > t.bytes.size()) return;
const uint16_t count = t.u16(offset);
const std::size_t end = offset + 2 + std::size_t(count) * 12;
if (end > t.bytes.size()) return;
for (uint16_t i = 0; i < count; i++) {
const std::size_t entry = offset + 2 + std::size_t(i) * 12;
const uint16_t tag = t.u16(entry);
const uint16_t type = t.u16(entry + 2);
const uint32_t n = t.u32(entry + 4);
switch (tag) {
case TAG_EXPOSURE_TIME: {
const auto values = readRationals(t, entry, type, n);
if (values && !values->empty() && !meta.exposureTime) meta.exposureTime = (*values)[0];
break;
}
case TAG_F_NUMBER: {
const auto values = readRationals(t, entry, type, n);
if (values && !values->empty() && !meta.fNumber) meta.fNumber = (*values)[0];
break;
}
case TAG_ISO:
if (!meta.iso) meta.iso = readNumber(t, entry, type, n);
break;
case TAG_DATE_TIME_ORIGINAL:
// Phone photos usually carry the real capture time here only.
if (!meta.dateTime) meta.dateTime = readAscii(t, entry, type, n);
break;
case TAG_FOCAL_LENGTH: {
const auto values = readRationals(t, entry, type, n);
if (values && !values->empty() && !meta.focalLength) meta.focalLength = (*values)[0];
break;
}
default: break;
}
}
}
static void readGpsIfd(const Tiff& t, std::size_t offset, ExifMetadata& meta) {
if (offset < 8 || offset + 2 > t.bytes.size()) return;
const uint16_t count = t.u16(offset);
const std::size_t end = offset + 2 + std::size_t(count) * 12;
if (end > t.bytes.size()) return;
std::string latRef = "N";
std::string lonRef = "E";
std::optional<std::vector<double>> lat;
std::optional<std::vector<double>> lon;
for (uint16_t i = 0; i < count; i++) {
const std::size_t entry = offset + 2 + std::size_t(i) * 12;
const uint16_t tag = t.u16(entry);
const uint16_t type = t.u16(entry + 2);
const uint32_t n = t.u32(entry + 4);
switch (tag) {
case TAG_GPS_LAT_REF: {
const auto ref = readAscii(t, entry, type, n);
if (ref && !ref->empty()) latRef = std::string(1, char(std::toupper((*ref)[0])));
break;
}
case TAG_GPS_LAT:
lat = readRationals(t, entry, type, n);
break;
case TAG_GPS_LON_REF: {
const auto ref = readAscii(t, entry, type, n);
if (ref && !ref->empty()) lonRef = std::string(1, char(std::toupper((*ref)[0])));
break;
}
case TAG_GPS_LON:
lon = readRationals(t, entry, type, n);
break;
default: break;
}
}
if (lat && !meta.gpsLatitude) meta.gpsLatitude = formatGpsCoordinate(*lat, latRef);
if (lon && !meta.gpsLongitude) meta.gpsLongitude = formatGpsCoordinate(*lon, lonRef);
}
// 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 parseXmp(const Bytes& payload, ExifMetadata& meta) {
const std::string text(payload.begin(), payload.end());
const std::size_t close = std::string::npos;
for (const char* tag : {":CreatorTool>", ":Software>"}) {
const std::string needle(tag);
std::size_t at = text.find(needle);
while (at != close) {
const std::size_t valueStart = at + needle.size();
const std::size_t valueEnd = text.find('<', valueStart);
if (valueEnd == close) break;
std::string value = text.substr(valueStart, valueEnd - valueStart);
// Trim whitespace, as the TS `.trim()` does.
std::size_t b = value.find_first_not_of(" \t\r\n");
if (b != close) {
std::size_t e = value.find_last_not_of(" \t\r\n");
value = value.substr(b, e - b + 1);
if (!value.empty()) {
if (!meta.software) meta.software = value;
return;
}
}
at = text.find(needle, valueEnd);
}
}
}
// --- JPEG parse ---------------------------------------------------------------
static void parseJpeg(const Bytes& bytes, ExifMetadata& meta) {
const auto segments = collectJpegSegments(bytes);
if (!segments) return;
for (const auto& seg : *segments) {
if (seg.marker == 0xe1) {
if (hasSignature(bytes, seg.payloadStart, EXIF_SIGNATURE)) {
parseTiff(Bytes(bytes.begin() + seg.payloadStart + sizeof(EXIF_SIGNATURE),
bytes.begin() + seg.end),
meta);
} else {
for (const char* sig : XMP_SIGNATURES) {
if (hasSignature(bytes, seg.payloadStart, sig)) {
parseXmp(Bytes(bytes.begin() + seg.payloadStart, bytes.begin() + seg.end), meta);
break;
}
}
}
} else if (seg.marker >= 0xc0 && seg.marker <= 0xcf && seg.marker != 0xc4 &&
seg.marker != 0xc8 && seg.marker != 0xcc &&
seg.end - seg.payloadStart >= 5) {
// Start-of-frame header: precision(1), height(2), width(2), big-endian.
if (!meta.imageHeight) meta.imageHeight = u16be(bytes, seg.payloadStart + 1);
if (!meta.imageWidth) meta.imageWidth = u16be(bytes, seg.payloadStart + 3);
}
}
}
// --- PNG parse ----------------------------------------------------------------
static void parsePngTextChunk(const std::string& kind, const Bytes& data, ExifMetadata& meta) {
const std::size_t nul = std::distance(data.begin(), std::find(data.begin(), data.end(), 0));
if (nul < 1 || nul >= data.size()) return;
const std::string keyword(data.begin(), data.begin() + nul);
if (keyword != "Software") return;
std::size_t p;
if (kind == "tEXt") {
p = nul + 1;
} else {
// iTXt: keyword\0 compressionFlag(1) compressionMethod(1) languageTag\0
// translatedKeyword\0 text(utf-8). Only uncompressed text is read.
if (nul + 1 >= data.size() || data[nul + 1] != 0) return;
p = nul + 3;
// Skip the language tag and the translated keyword (both NUL-terminated).
for (int skipped = 0; skipped < 2; skipped++) {
const std::size_t next = std::distance(data.begin(), std::find(data.begin() + p, data.end(), 0));
if (next >= data.size()) return;
p = next + 1;
}
}
std::string value(data.begin() + p, data.end());
std::size_t b = value.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return;
std::size_t e = value.find_last_not_of(" \t\r\n");
value = value.substr(b, e - b + 1);
if (!value.empty() && !meta.software) meta.software = value;
}
static void parsePng(const Bytes& bytes, ExifMetadata& meta) {
std::size_t off = 8;
while (off + 8 <= bytes.size()) {
const uint32_t length = u32be(bytes, off);
if (length > bytes.size() - off - 12) break; // corrupted chunk
const std::string type(bytes.begin() + off + 4, bytes.begin() + off + 8);
const Bytes data(bytes.begin() + off + 8, bytes.begin() + off + 8 + length);
if (type == "IHDR") {
if (!meta.imageWidth) meta.imageWidth = u32be(bytes, off + 8);
if (!meta.imageHeight) meta.imageHeight = u32be(bytes, off + 12);
} else if (type == "tEXt" || type == "iTXt") {
parsePngTextChunk(type, data, meta);
} else if (type == "eXIf") {
parseTiff(data, meta);
}
if (type == "IEND") break;
off += 12 + length;
}
}
// --- strip --------------------------------------------------------------------
static Bytes concat(const std::vector<Bytes>& parts) {
std::size_t total = 0;
for (const auto& p : parts) total += p.size();
Bytes out;
out.reserve(total);
for (const auto& p : parts) out.insert(out.end(), p.begin(), p.end());
return out;
}
static Bytes stripJpeg(const Bytes& bytes) {
const auto segments = collectJpegSegments(bytes);
if (!segments) return bytes; // corrupted walk - copy verbatim
std::vector<Bytes> parts{Bytes(bytes.begin(), bytes.begin() + 2)};
for (const auto& seg : *segments) {
if (seg.marker == 0xda) {
// SOS header + entropy data + EOI are copied verbatim to the end.
parts.push_back(Bytes(bytes.begin() + seg.start, bytes.end()));
break;
}
if (isJpegMetadataSegment(bytes, seg)) continue;
parts.push_back(Bytes(bytes.begin() + seg.start, bytes.begin() + seg.end));
}
return concat(parts);
}
static Bytes stripPng(const Bytes& bytes) {
std::vector<Bytes> parts{Bytes(bytes.begin(), bytes.begin() + 8)};
std::size_t off = 8;
while (off + 8 <= bytes.size()) {
const uint32_t length = u32be(bytes, off);
if (length > bytes.size() - off - 12) break;
const std::string type(bytes.begin() + off + 4, bytes.begin() + off + 8);
// tEXt / iTXt / eXIf are the metadata carriers; everything else (IHDR,
// PLTE, IDAT, ...) is copied byte-for-byte, CRC included.
if (type != "tEXt" && type != "iTXt" && type != "eXIf") {
parts.push_back(Bytes(bytes.begin() + off, bytes.begin() + off + 12 + length));
}
off += 12 + length;
}
if (off < bytes.size()) parts.push_back(Bytes(bytes.begin() + off, bytes.end()));
return concat(parts);
}
// --- public API ---------------------------------------------------------------
Bytes stripMetadata(const Bytes& buffer) {
if (buffer.size() < 8) throw std::invalid_argument("File is too small to be a valid image.");
if (isJpeg(buffer)) return stripJpeg(buffer);
if (isPng(buffer)) return stripPng(buffer);
throw std::invalid_argument("Unsupported format: only JPEG and PNG images are supported.");
}
ExifMetadata parseMetadata(const Bytes& buffer) {
if (buffer.size() < 8) throw std::invalid_argument("File is too small to be a valid image.");
if (!isJpeg(buffer) && !isPng(buffer)) {
throw std::invalid_argument("Unsupported format: only JPEG and PNG images are supported.");
}
ExifMetadata meta;
meta.stripped = stripMetadata(buffer);
if (isJpeg(buffer)) parseJpeg(buffer, meta);
else parsePng(buffer, meta);
return meta;
}
} // namespace exif_stripper
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 →