EXIF
A photo's EXIF: camera and lens, when it was taken, where (GPS), exposure and orientation, from JPEG, HEIC, PNG and WebP.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use exif.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use exif_read, exif_gps from exif (Quillo 0.6).
exif_read(path: str) → anyor an errorA photo's details from its EXIF: { make, model, lens, taken, gps, orientation, width, height, exposure, f_number, iso, focal_length, flash, software, artist, copyright }. taken is "2026-06-14T09:30:00" (local time of the camera, with its offset when known); gps is { lat, lon, altitude } in degrees and metres, or null; exposure is like "1/250" (seconds); orientation 1 to 8 (1 upright, 6 turned a quarter clockwise). A picture with no EXIF gives null.
exif_gps(path: str) → anyor an errorWhere a photo was taken: { lat, lon, altitude } (degrees, negative south and west; metres), or null when it has no position.
exif_tags(path: str) → mapor an errorEvery EXIF field of the picture as a map from the tag's name to its value as text ("Make" to "Quillo", "ExposureTime" to "1/250"), the main image's fields only. Empty for a picture with none.
Get it
$ quillo install exif # exif.qwer into this folder$ quillo install exif --example # and its example, exif.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/exif.qwer$ curl -O https://quillolang.org/plugins/files/exif.qwe$ curl -O https://quillolang.org/plugins/files/exif_sample.jpg$ curl -O https://quillolang.org/plugins/files/exif_sample.pngThen, in your program, all of it or only what you call:
use exif.qweruse exif_read, exif_gps from exif // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
kamadak-exif = "0.6"Example
// A photo library that reads where and when each photo was taken, and with what.//// quillo run exif.qwe then open http://localhost:8080/photouse "exif.qwer"let PHOTO = "exif_sample.jpg"route GET /photo { let e = exif_read(PHOTO)? ?? reply 404 "This photo carries no EXIF" { camera: `${e.make} ${e.model}`, taken: e.taken, settings: `${e.exposure} s at f/${e.f_number}, ISO ${e.iso}, ${e.focal_length} mm`, gps: e.gps, map: e.gps == null ? null : `https://www.openstreetmap.org/?mlat=${e.gps.lat}&mlon=${e.gps.lon}#map=16/${e.gps.lat}/${e.gps.lon}`, }}route GET /gps = exif_gps(PHOTO)route GET /tags = exif_tags(PHOTO)route GET /none = { exif: exif_read("exif_sample.png")?, gps: exif_gps("exif_sample.png")? }$ quillo run exif.qwe$ curl 'localhost:8080/photo'$ curl 'localhost:8080/gps'$ curl 'localhost:8080/none'Source
//! A photo's EXIF data: camera and lens, when it was taken, where (GPS), exposure and orientation, from JPEG, HEIC, PNG, WebP and TIFF files.//! version: 1.0.0//!//! [dependencies]//! kamadak-exif = "0.6"use exif::{In, Tag, Value as EV};use serde_json::{json, Map, Value};fn read(path: &str) -> Result<Option<exif::Exif>, String> { let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; let mut r = std::io::BufReader::new(f); match exif::Reader::new().read_from_container(&mut r) { Ok(e) => Ok(Some(e)), Err(exif::Error::NotFound(_)) | Err(exif::Error::BlankValue(_)) => Ok(None), Err(exif::Error::InvalidFormat(m)) if m.contains("Unknown image format") || m.contains("Broken") => Err(format!("{}: not a picture with EXIF ({})", path, m)), Err(e) => Err(format!("{}: {}", path, e)), }}fn text(e: &exif::Exif, tag: Tag) -> Option<String> { let f = e.get_field(tag, In::PRIMARY)?; match &f.value { EV::Ascii(v) => v.first().map(|b| String::from_utf8_lossy(b).trim().to_string()).filter(|s| !s.is_empty()), _ => Some(f.display_value().to_string()), }}fn num(e: &exif::Exif, tag: Tag) -> Option<f64> { let f = e.get_field(tag, In::PRIMARY)?; match &f.value { EV::Rational(v) => v.first().map(|r| r.to_f64()), EV::SRational(v) => v.first().map(|r| r.to_f64()), EV::Short(v) => v.first().map(|&x| x as f64), EV::Long(v) => v.first().map(|&x| x as f64), _ => None, } .filter(|x| x.is_finite())}fn round(x: f64, places: i32) -> f64 { let p = 10f64.powi(places); (x * p).round() / p}// Degrees, minutes and seconds as decimal degrees, negative to the south and west.fn coord(e: &exif::Exif, tag: Tag, r: Tag) -> Option<f64> { let f = e.get_field(tag, In::PRIMARY)?; let EV::Rational(v) = &f.value else { return None }; let dms: Vec<f64> = v.iter().map(|r| r.to_f64()).collect(); let deg = dms.first()? + dms.get(1).unwrap_or(&0.0) / 60.0 + dms.get(2).unwrap_or(&0.0) / 3600.0; let sign = match text(e, r).as_deref() { Some("S") | Some("W") => -1.0, _ => 1.0 }; Some(round(sign * deg, 6)).filter(|d| d.is_finite() && d.abs() <= 180.0)}fn gps_of(e: &exif::Exif) -> Value { match (coord(e, Tag::GPSLatitude, Tag::GPSLatitudeRef), coord(e, Tag::GPSLongitude, Tag::GPSLongitudeRef)) { (Some(lat), Some(lon)) => { let below = e.get_field(Tag::GPSAltitudeRef, In::PRIMARY).and_then(|f| f.value.get_uint(0)) == Some(1); let alt = num(e, Tag::GPSAltitude).map(|a| round(if below { -a } else { a }, 1)); json!({ "lat": lat, "lon": lon, "altitude": alt }) } _ => Value::Null, }}// "2026:06:14 09:30:00" as "2026-06-14T09:30:00", with the offset when the photo gives one.fn date(e: &exif::Exif) -> Option<String> { let d = text(e, Tag::DateTimeOriginal).or_else(|| text(e, Tag::DateTimeDigitized)).or_else(|| text(e, Tag::DateTime))?; if d.len() < 19 || !d.is_ascii() || d.starts_with("0000") { return None; } let mut s = format!("{}-{}-{}T{}", &d[0..4], &d[5..7], &d[8..10], &d[11..19]); if let Some(off) = text(e, Tag::OffsetTimeOriginal).or_else(|| text(e, Tag::OffsetTime)) { s.push_str(&off); } Some(s)}/// A photo's details from its EXIF: `{ make, model, lens, taken, gps, orientation, width, height, exposure, f_number, iso, focal_length, flash, software, artist, copyright }`./// `taken` is "2026-06-14T09:30:00" (local time of the camera, with its offset when known); `gps` is `{ lat, lon, altitude }` in degrees and metres, or null;/// `exposure` is like "1/250" (seconds); `orientation` 1 to 8 (1 upright, 6 turned a quarter clockwise). A picture with no EXIF gives null.pub fn exif_read(path: &str) -> Result<Value, String> { let Some(e) = read(path)? else { return Ok(Value::Null) }; let exposure = e.get_field(Tag::ExposureTime, In::PRIMARY).and_then(|f| match &f.value { EV::Rational(v) => v.first().map(|r| if r.num == 1 || r.num == 0 || r.denom == 0 { format!("{}/{}", r.num, r.denom) } else if r.to_f64() < 1.0 { format!("1/{}", (1.0 / r.to_f64()).round()) } else { format!("{}", round(r.to_f64(), 1)) }), _ => None, }); let dim = |a: Tag, b: Tag| e.get_field(a, In::PRIMARY).or_else(|| e.get_field(b, In::PRIMARY)).and_then(|f| f.value.get_uint(0)); Ok(json!({ "make": text(&e, Tag::Make), "model": text(&e, Tag::Model), "lens": text(&e, Tag::LensModel), "taken": date(&e), "gps": gps_of(&e), "orientation": e.get_field(Tag::Orientation, In::PRIMARY).and_then(|f| f.value.get_uint(0)).unwrap_or(1), "width": dim(Tag::PixelXDimension, Tag::ImageWidth), "height": dim(Tag::PixelYDimension, Tag::ImageLength), "exposure": exposure, "f_number": num(&e, Tag::FNumber).map(|x| round(x, 1)), "iso": num(&e, Tag::PhotographicSensitivity).map(|x| x as i64), "focal_length": num(&e, Tag::FocalLength).map(|x| round(x, 1)), "flash": e.get_field(Tag::Flash, In::PRIMARY).and_then(|f| f.value.get_uint(0)).map(|v| v & 1 == 1), "software": text(&e, Tag::Software), "artist": text(&e, Tag::Artist), "copyright": text(&e, Tag::Copyright), }))}/// Where a photo was taken: `{ lat, lon, altitude }` (degrees, negative south and west; metres), or null when it has no position.pub fn exif_gps(path: &str) -> Result<Value, String> { Ok(read(path)?.map(|e| gps_of(&e)).unwrap_or(Value::Null))}/// Every EXIF field of the picture as a map from the tag's name to its value as text ("Make" to "Quillo", "ExposureTime" to "1/250"), the main image's fields only./// Empty for a picture with none.pub fn exif_tags(path: &str) -> Result<std::collections::HashMap<String, String>, String> { let mut m = Map::new(); if let Some(e) = read(path)? { for f in e.fields().filter(|f| f.ifd_num == In::PRIMARY).take(2000) { let v = match &f.value { EV::Ascii(v) => v.iter().map(|b| String::from_utf8_lossy(b).to_string()).collect::<Vec<_>>().join(", "), EV::Undefined(b, _) if b.len() > 64 => format!("({} bytes)", b.len()), _ => f.display_value().with_unit(&e).to_string(), }; m.insert(f.tag.to_string(), Value::String(v)); } } Ok(m.into_iter().map(|(k, v)| (k, v.as_str().unwrap_or("").to_string())).collect())}Related
exif_strip.qwer · 1.0.0Photos made safe to publish: GPS, camera and other EXIF, XMP, IPTC and comments taken out of JPEG, PNG and WebP, without re-encoding.6 views · 7 downloadsImage edits image_ops.qwer · 1.0.0Crop, rotate, flip, grayscale, blur, and phone photos turned upright by their EXIF orientation, saved as JPEG, PNG, WebP or GIF.7 views · 7 downloadsOCR ocr.qwer · 1.0.0Text read from pictures in pure Rust: photos and scans of documents, receipts, labels and screenshots to plain text, row by row or line by line with boxes, parts of the page read by range (pixels or percentages), several form fields at once, only the characters you allow, and phone photos turned upright first.13 views · 9 downloadsWatermark watermark.qwer · 1.0.0A logo laid over photos at a corner or the centre, or tiled across, with the opacity you choose, for publishing pictures with your mark.8 views · 8 downloadsCaptions on images caption.qwer · 1.0.0Text drawn on pictures with a built-in font: captions at the top, middle or bottom, wrapped to fit, with an outline or a band behind them, meme-style top and bottom lines, and text measured before drawing.7 views · 7 downloadsCollage collage.qwer · 1.0.0Several pictures made into one: a grid of equal cells with gaps, or justified rows that keep each picture's shape, on your background.7 views · 7 downloadsThread 0 posts
On EXIF: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.