Created
May 5, 2025 13:54
-
-
Save georg-jung/eb70ce22991c7d63a4113bcb0ba4e6a3 to your computer and use it in GitHub Desktop.
Rust strip-thumb by gpt o3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "strip_thumb" | |
version = "0.2.0" | |
edition = "2021" | |
[dependencies] | |
img-parts = "0.3" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/main.rs | |
use std::{ | |
env, | |
fs::{self, File}, | |
io, | |
path::Path, | |
}; | |
use img_parts::jpeg::Jpeg; | |
use img_parts::{Bytes, ImageEXIF}; | |
fn main() -> io::Result<()> { | |
/* ---------- very tiny CLI ---------- */ | |
let mut args = env::args().skip(1); // leave program name behind | |
let mut verbose = false; | |
let path = match args.next() { | |
Some(flag) if flag == "-v" || flag == "--verbose" => { | |
verbose = true; | |
args.next().expect("usage: strip_thumb [-v] <file.jpg>") | |
} | |
Some(p) => p, | |
None => { | |
eprintln!("usage: strip_thumb [-v] <file.jpg>"); | |
std::process::exit(1); | |
} | |
}; | |
if verbose { | |
eprintln!("→ opening {path}"); | |
} | |
let jpeg_bytes = fs::read(&path)?; | |
let mut jpg = Jpeg::from_bytes(jpeg_bytes.into()) | |
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; | |
/* ---------- operate on EXIF ---------- */ | |
match jpg.exif() { | |
None => { | |
eprintln!("Image has no EXIF block – nothing to do"); | |
return Ok(()); | |
} | |
Some(raw) => { | |
let mut exif = raw.as_ref().to_vec(); | |
if !strip_thumbnail(&mut exif, verbose) { | |
eprintln!("No embedded thumbnail found – nothing to do"); | |
return Ok(()); | |
} | |
jpg.set_exif(Some(Bytes::from(exif))); | |
} | |
} | |
/* ---------- atomic write‑back ---------- */ | |
let tmp_path = Path::new(&path).with_extension("tmp"); | |
{ | |
let tmp = File::create(&tmp_path)?; | |
jpg.encoder().write_to(tmp)?; | |
} | |
fs::rename(&tmp_path, &path)?; | |
println!("✔ thumbnail removed, file size reduced!"); | |
Ok(()) | |
} | |
/* ------------------------------------------------------------------------- */ | |
/// Remove compressed **or** uncompressed thumbnails from an EXIF block. | |
/// With `verbose = true` the function prints every step to `stderr`. | |
/// Returns `true` iff a thumbnail was found and stripped. | |
fn strip_thumbnail(exif: &mut Vec<u8>, verbose: bool) -> bool { | |
macro_rules! say { ($($tt:tt)*) => { if verbose { eprintln!($($tt)*); } } } | |
/* ---------- locate TIFF header ---------- */ | |
let (tiff, has_prefix) = if exif.starts_with(b"Exif\0\0") { | |
say!("→ APP1 starts with \"Exif\\0\\0\" (standard)"); | |
(6, true) // TIFF header is 6 bytes in | |
} else { | |
say!("→ APP1 *does NOT* start with \"Exif\\0\\0\" – assuming TIFF starts immediately"); | |
(0, false) // entire block is the TIFF header | |
}; | |
if exif.len() < tiff + 8 { | |
say!("✗ EXIF block too small (<8 bytes after prefix)"); | |
return false; | |
} | |
/* ---------- endian flag ---------- */ | |
let le = match &exif[tiff..tiff + 2] { | |
b"II" => { say!("→ little‑endian TIFF"); true } | |
b"MM" => { say!("→ big‑endian TIFF"); false } | |
_ => { | |
say!("✗ corrupted TIFF header (no II/MM)"); | |
return false; | |
} | |
}; | |
/* endian helpers (stateless, so borrow issues are gone) */ | |
let read_u16 = |buf: &[u8], off: usize| -> u16 { | |
if le { u16::from_le_bytes([buf[off], buf[off + 1]]) } | |
else { u16::from_be_bytes([buf[off], buf[off + 1]]) } | |
}; | |
let read_u32 = |buf: &[u8], off: usize| -> u32 { | |
if le { u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]) } | |
else { u32::from_be_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]) } | |
}; | |
let write_u32 = |buf: &mut [u8], off: usize, v: u32| { | |
let b = if le { v.to_le_bytes() } else { v.to_be_bytes() }; | |
buf[off..off + 4].copy_from_slice(&b); | |
}; | |
/* ---------- locate IFD 0 ---------- */ | |
let ifd0_off = read_u32(&exif, tiff + 4) as usize; | |
say!("IFD0 @ 0x{ifd0_off:08x}"); | |
let ifd0 = tiff + ifd0_off; | |
if ifd0 + 2 > exif.len() { return false; } | |
let n0 = read_u16(&exif, ifd0) as usize; | |
say!("IFD0 has {n0} entries"); | |
let after_ifd0 = ifd0 + 2 + n0 * 12; | |
if after_ifd0 + 4 > exif.len() { return false; } | |
let ifd1_off = read_u32(&exif, after_ifd0) as usize; | |
if ifd1_off == 0 { | |
say!("✗ IFD1 pointer is 0 – no thumbnail directory"); | |
return false; | |
} | |
let ifd1 = tiff + ifd1_off; | |
say!("IFD1 @ 0x{ifd1_off:08x}"); | |
if ifd1 + 2 > exif.len() { return false; } | |
/* ---------- parse IFD 1 ---------- */ | |
let n1 = read_u16(&exif, ifd1) as usize; | |
say!("IFD1 has {n1} entries"); | |
if ifd1 + 2 + n1 * 12 + 4 > exif.len() { return false; } | |
let mut data_off: Option<usize> = None; | |
let mut data_len: Option<usize> = None; | |
for i in 0..n1 { | |
let ent = ifd1 + 2 + i * 12; | |
let tag = read_u16(&exif, ent); | |
let typ = read_u16(&exif, ent + 2); | |
let cnt = read_u32(&exif, ent + 4); | |
let val4 = read_u32(&exif, ent + 8); // fits for LONG & SLONG | |
say!(" tag 0x{tag:04x} type {typ} cnt {cnt} val 0x{val4:08x}"); | |
match tag { | |
/* Compressed JPEG thumbnail */ | |
0x0201 => { data_off = Some(val4 as usize); write_u32(exif, ent + 8, 0); } | |
0x0202 => { data_len = Some(val4 as usize); write_u32(exif, ent + 8, 0); } | |
/* Uncompressed thumbnail strips (we handle single‑strip only) */ | |
0x0111 if cnt == 1 => { | |
data_off = Some(val4 as usize); | |
write_u32(exif, ent + 8, 0); | |
} | |
0x0117 if cnt == 1 => { | |
data_len = Some(val4 as usize); | |
write_u32(exif, ent + 8, 0); | |
} | |
_ => {} | |
} | |
} | |
let (start, len) = match (data_off, data_len) { | |
(Some(o), Some(l)) if l > 0 => (tiff + o, l), | |
_ => { | |
say!("✗ did not get BOTH offset and length – giving up"); | |
return false; | |
} | |
}; | |
let end = start + len; | |
if end > exif.len() { | |
say!("✗ thumbnail claims bytes past the EXIF block – corrupted?"); | |
return false; | |
} | |
say!("Thumbnail bytes 0x{start:08x} .. 0x{end:08x} (len {len})"); | |
/* ---------- cut the bytes out ---------- */ | |
if end == exif.len() { | |
exif.truncate(start); | |
say!("→ truncated EXIF from 0x{start:08x}"); | |
} else { | |
exif.splice(start..end, std::iter::empty()); | |
say!("→ spliced them out (thumbnail was NOT at the end!)"); | |
} | |
/* ---------- unlink IFD 1 ---------- */ | |
write_u32(exif, after_ifd0, 0); | |
say!("IFD0 → IFD1 pointer zeroed"); | |
true | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment