Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
963 lines
30 KiB
Rust
963 lines
30 KiB
Rust
//! Extração de metadados de arquivos recuperados: EXIF (câmera, datas, GPS),
|
||
//! informações de PDF, propriedades de documentos Office/OpenDocument e
|
||
//! dados de vídeo MP4/MOV. Tudo é leitura posicional, sem gravar em disco.
|
||
|
||
use crate::recover::read_found_at;
|
||
use crate::source::Source;
|
||
use crate::types::Found;
|
||
use serde::{Deserialize, Serialize};
|
||
use std::io::Read;
|
||
|
||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||
pub struct Meta {
|
||
pub kind: String,
|
||
pub fields: Vec<(String, String)>,
|
||
/// (latitude, longitude) em graus decimais.
|
||
pub gps: Option<(f64, f64)>,
|
||
/// Data em que a foto foi tirada (EXIF), s desde 1970 (hora da câmera lida como UTC).
|
||
#[serde(default)]
|
||
pub taken: Option<i64>,
|
||
#[serde(default)]
|
||
pub camera: Option<String>,
|
||
#[serde(default)]
|
||
pub px: Option<(u32, u32)>,
|
||
/// Orientação EXIF (1..8).
|
||
#[serde(default)]
|
||
pub orientation: u16,
|
||
}
|
||
|
||
impl Meta {
|
||
fn add(&mut self, k: &str, v: impl Into<String>) {
|
||
let v = v.into();
|
||
let v = v.trim().to_string();
|
||
if !v.is_empty() {
|
||
self.fields.push((k.to_string(), v));
|
||
}
|
||
}
|
||
}
|
||
|
||
struct Fr<'a> {
|
||
src: &'a dyn Source,
|
||
f: &'a Found,
|
||
}
|
||
|
||
impl Fr<'_> {
|
||
fn read(&self, off: u64, len: usize) -> Vec<u8> {
|
||
let mut out = vec![0u8; len.min(self.f.size.saturating_sub(off) as usize)];
|
||
let mut got = 0;
|
||
while got < out.len() {
|
||
match read_found_at(self.src, self.f, off + got as u64, &mut out[got..]) {
|
||
Ok(0) | Err(_) => break,
|
||
Ok(n) => got += n,
|
||
}
|
||
}
|
||
out.truncate(got);
|
||
out
|
||
}
|
||
}
|
||
|
||
pub fn extract(src: &dyn Source, f: &Found) -> Meta {
|
||
let fr = Fr { src, f };
|
||
let mut m = Meta::default();
|
||
match f.ext.to_ascii_lowercase().as_str() {
|
||
"jpg" | "jpeg" => {
|
||
m.kind = "Imagem JPEG".into();
|
||
jpeg(&fr, &mut m);
|
||
}
|
||
"png" => {
|
||
m.kind = "Imagem PNG".into();
|
||
let h = fr.read(0, 32);
|
||
if h.len() >= 24 && &h[12..16] == b"IHDR" {
|
||
m.px = Some((be32(&h[16..]), be32(&h[20..])));
|
||
m.add(
|
||
"Dimensões",
|
||
format!("{} × {} px", be32(&h[16..]), be32(&h[20..])),
|
||
);
|
||
}
|
||
}
|
||
"gif" => {
|
||
m.kind = "Imagem GIF".into();
|
||
let h = fr.read(0, 16);
|
||
if h.len() >= 10 {
|
||
m.px = Some((le16(&h[6..]) as u32, le16(&h[8..]) as u32));
|
||
m.add(
|
||
"Dimensões",
|
||
format!("{} × {} px", le16(&h[6..]), le16(&h[8..])),
|
||
);
|
||
}
|
||
}
|
||
"bmp" => {
|
||
m.kind = "Imagem BMP".into();
|
||
let h = fr.read(0, 32);
|
||
if h.len() >= 26 {
|
||
m.px = Some((le32(&h[18..]), (le32(&h[22..]) as i32).unsigned_abs()));
|
||
m.add(
|
||
"Dimensões",
|
||
format!(
|
||
"{} × {} px",
|
||
le32(&h[18..]),
|
||
(le32(&h[22..]) as i32).unsigned_abs()
|
||
),
|
||
);
|
||
}
|
||
}
|
||
"pdf" => {
|
||
m.kind = "Documento PDF".into();
|
||
pdf(&fr, &mut m);
|
||
}
|
||
"docx" | "xlsx" | "pptx" => {
|
||
m.kind = "Documento Office".into();
|
||
ooxml(&fr, &mut m);
|
||
}
|
||
"odt" | "ods" | "odp" => {
|
||
m.kind = "Documento OpenDocument".into();
|
||
odf(&fr, &mut m);
|
||
}
|
||
"mp4" | "mov" | "m4v" | "m4a" | "3gp" => {
|
||
m.kind = "Mídia MP4/MOV".into();
|
||
mp4(&fr, &mut m);
|
||
}
|
||
_ => {}
|
||
}
|
||
m
|
||
}
|
||
|
||
fn be16(b: &[u8]) -> u16 {
|
||
u16::from_be_bytes([b[0], b[1]])
|
||
}
|
||
fn be32(b: &[u8]) -> u32 {
|
||
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
|
||
}
|
||
fn le16(b: &[u8]) -> u16 {
|
||
u16::from_le_bytes([b[0], b[1]])
|
||
}
|
||
fn le32(b: &[u8]) -> u32 {
|
||
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
|
||
}
|
||
|
||
// ------------------------------------------------------------------ EXIF
|
||
|
||
fn jpeg(fr: &Fr, m: &mut Meta) {
|
||
let d = fr.read(0, 256 * 1024);
|
||
jpeg_bytes(&d, m);
|
||
}
|
||
|
||
/// Lê os segmentos de cabeçalho de um JPEG (já em memória): dimensões e EXIF.
|
||
pub fn jpeg_bytes(d: &[u8], m: &mut Meta) {
|
||
let mut p = 2usize;
|
||
while p + 4 <= d.len() && d[p] == 0xFF {
|
||
let mk = d[p + 1];
|
||
if mk == 0xDA || mk == 0xD9 {
|
||
break;
|
||
}
|
||
let len = be16(&d[p + 2..]) as usize;
|
||
if len < 2 || p + 2 + len > d.len() {
|
||
break;
|
||
}
|
||
if mk == 0xE1 && d[p + 4..].starts_with(b"Exif\0\0") {
|
||
exif(&d[p + 10..p + 2 + len], m);
|
||
}
|
||
if (mk == 0xC0 || mk == 0xC2) && p + 9 <= d.len() {
|
||
m.px = Some((be16(&d[p + 7..]) as u32, be16(&d[p + 5..]) as u32));
|
||
m.add(
|
||
"Dimensões",
|
||
format!("{} × {} px", be16(&d[p + 7..]), be16(&d[p + 5..])),
|
||
);
|
||
}
|
||
p += 2 + len;
|
||
}
|
||
}
|
||
|
||
/// Miniatura JPEG embutida no EXIF (IFD1), útil quando a imagem principal está danificada.
|
||
pub fn exif_thumbnail(d: &[u8]) -> Option<Vec<u8>> {
|
||
let mut p = 2usize;
|
||
while p + 4 <= d.len() && d[p] == 0xFF {
|
||
let mk = d[p + 1];
|
||
if mk == 0xDA || mk == 0xD9 {
|
||
return None;
|
||
}
|
||
let len = be16(&d[p + 2..]) as usize;
|
||
if len < 2 || p + 2 + len > d.len() {
|
||
return None;
|
||
}
|
||
if mk == 0xE1 && d[p + 4..].starts_with(b"Exif\0\0") {
|
||
let tiff = &d[p + 10..p + 2 + len];
|
||
if tiff.len() < 8 {
|
||
return None;
|
||
}
|
||
let le = match &tiff[0..2] {
|
||
b"II" => true,
|
||
b"MM" => false,
|
||
_ => return None,
|
||
};
|
||
let t = Tiff { d: tiff, le };
|
||
let ifd0 = t.u32(4)? as usize;
|
||
let n = t.u16(ifd0)? as usize;
|
||
let ifd1 = t.u32(ifd0 + 2 + n * 12)? as usize;
|
||
if ifd1 == 0 {
|
||
return None;
|
||
}
|
||
let (mut off, mut length) = (None, None);
|
||
for e in t.ifd(ifd1) {
|
||
match e.tag {
|
||
0x0201 => off = t.u32(e.pos),
|
||
0x0202 => length = t.u32(e.pos),
|
||
_ => {}
|
||
}
|
||
}
|
||
let (o, l) = (off? as usize, length? as usize);
|
||
return tiff.get(o..o + l).map(|b| b.to_vec());
|
||
}
|
||
p += 2 + len;
|
||
}
|
||
None
|
||
}
|
||
|
||
struct Tiff<'a> {
|
||
d: &'a [u8],
|
||
le: bool,
|
||
}
|
||
|
||
struct Entry {
|
||
tag: u16,
|
||
typ: u16,
|
||
count: u32,
|
||
pos: usize, // onde estão os dados do valor dentro do TIFF
|
||
}
|
||
|
||
impl Tiff<'_> {
|
||
fn u16(&self, o: usize) -> Option<u16> {
|
||
let b = self.d.get(o..o + 2)?;
|
||
Some(if self.le {
|
||
u16::from_le_bytes([b[0], b[1]])
|
||
} else {
|
||
u16::from_be_bytes([b[0], b[1]])
|
||
})
|
||
}
|
||
fn u32(&self, o: usize) -> Option<u32> {
|
||
let b = self.d.get(o..o + 4)?;
|
||
Some(if self.le {
|
||
u32::from_le_bytes([b[0], b[1], b[2], b[3]])
|
||
} else {
|
||
u32::from_be_bytes([b[0], b[1], b[2], b[3]])
|
||
})
|
||
}
|
||
fn ifd(&self, off: usize) -> Vec<Entry> {
|
||
let mut v = vec![];
|
||
let Some(n) = self.u16(off) else { return v };
|
||
for i in 0..(n as usize).min(200) {
|
||
let e = off + 2 + i * 12;
|
||
let (Some(tag), Some(typ), Some(count)) =
|
||
(self.u16(e), self.u16(e + 2), self.u32(e + 4))
|
||
else {
|
||
break;
|
||
};
|
||
let size = match typ {
|
||
1 | 2 | 6 | 7 => 1,
|
||
3 | 8 => 2,
|
||
4 | 9 | 11 => 4,
|
||
5 | 10 | 12 => 8,
|
||
_ => 1,
|
||
} * count as usize;
|
||
let pos = if size <= 4 {
|
||
e + 8
|
||
} else {
|
||
self.u32(e + 8).unwrap_or(0) as usize
|
||
};
|
||
v.push(Entry {
|
||
tag,
|
||
typ,
|
||
count,
|
||
pos,
|
||
});
|
||
}
|
||
v
|
||
}
|
||
fn ascii(&self, e: &Entry) -> String {
|
||
let end = (e.pos + e.count as usize).min(self.d.len());
|
||
let b = self.d.get(e.pos..end).unwrap_or(&[]);
|
||
String::from_utf8_lossy(b)
|
||
.trim_end_matches('\0')
|
||
.trim()
|
||
.to_string()
|
||
}
|
||
fn rational(&self, e: &Entry, i: usize) -> Option<f64> {
|
||
let o = e.pos + i * 8;
|
||
let (n, d) = (self.u32(o)?, self.u32(o + 4)?);
|
||
(d != 0).then(|| n as f64 / d as f64)
|
||
}
|
||
fn num(&self, e: &Entry) -> Option<u32> {
|
||
match e.typ {
|
||
3 => self.u16(e.pos).map(u32::from),
|
||
4 => self.u32(e.pos),
|
||
_ => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn exif(d: &[u8], m: &mut Meta) {
|
||
if d.len() < 8 {
|
||
return;
|
||
}
|
||
let le = match &d[0..2] {
|
||
b"II" => true,
|
||
b"MM" => false,
|
||
_ => return,
|
||
};
|
||
let t = Tiff { d, le };
|
||
let Some(ifd0) = t.u32(4) else { return };
|
||
let mut exif_ptr = None;
|
||
let mut gps_ptr = None;
|
||
let (mut make, mut model) = (String::new(), String::new());
|
||
for e in t.ifd(ifd0 as usize) {
|
||
match e.tag {
|
||
0x010F => {
|
||
make = t.ascii(&e);
|
||
m.add("Fabricante da câmera", make.clone());
|
||
}
|
||
0x0110 => {
|
||
model = t.ascii(&e);
|
||
m.add("Modelo da câmera", model.clone());
|
||
}
|
||
0x0112 => m.orientation = t.num(&e).unwrap_or(1) as u16,
|
||
0x0131 => m.add("Software", t.ascii(&e)),
|
||
0x0132 => m.add("Data (EXIF)", exif_date(&t.ascii(&e))),
|
||
0x013B => m.add("Autor", t.ascii(&e)),
|
||
0x8298 => m.add("Copyright", t.ascii(&e)),
|
||
0x8769 => exif_ptr = t.u32(e.pos),
|
||
0x8825 => gps_ptr = t.u32(e.pos),
|
||
_ => {}
|
||
}
|
||
}
|
||
let first = make.split_whitespace().next().unwrap_or("").to_lowercase();
|
||
let cam = if model.is_empty() {
|
||
make.clone()
|
||
} else if make.is_empty() || (!first.is_empty() && model.to_lowercase().starts_with(&first)) {
|
||
model.clone()
|
||
} else {
|
||
format!("{make} {model}")
|
||
};
|
||
if !cam.trim().is_empty() {
|
||
m.camera = Some(cam.trim().to_string());
|
||
}
|
||
if let Some(o) = exif_ptr {
|
||
let (mut w, mut h) = (None, None);
|
||
for e in t.ifd(o as usize) {
|
||
match e.tag {
|
||
0x9003 => {
|
||
m.taken = exif_epoch(&t.ascii(&e));
|
||
m.add("Foto tirada em", exif_date(&t.ascii(&e)));
|
||
}
|
||
0x9004 => m.add("Digitalizada em", exif_date(&t.ascii(&e))),
|
||
0xA434 => m.add("Lente", t.ascii(&e)),
|
||
0xA431 => m.add("Nº de série do corpo", t.ascii(&e)),
|
||
0xA430 => m.add("Proprietário da câmera", t.ascii(&e)),
|
||
0xA002 => w = t.num(&e),
|
||
0xA003 => h = t.num(&e),
|
||
0x829A => {
|
||
if let Some(v) = t.rational(&e, 0) {
|
||
m.add(
|
||
"Exposição",
|
||
if v > 0.0 && v < 1.0 {
|
||
format!("1/{:.0} s", 1.0 / v)
|
||
} else {
|
||
format!("{v:.1} s")
|
||
},
|
||
);
|
||
}
|
||
}
|
||
0x829D => {
|
||
if let Some(v) = t.rational(&e, 0) {
|
||
m.add("Abertura", format!("f/{v:.1}"));
|
||
}
|
||
}
|
||
0x8827 => {
|
||
if let Some(v) = t.num(&e) {
|
||
m.add("ISO", v.to_string());
|
||
}
|
||
}
|
||
0x920A => {
|
||
if let Some(v) = t.rational(&e, 0) {
|
||
m.add("Distância focal", format!("{v:.0} mm"));
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
if let (Some(w), Some(h)) = (w, h) {
|
||
if m.px.is_none() {
|
||
m.px = Some((w, h));
|
||
}
|
||
if !m.fields.iter().any(|(k, _)| k == "Dimensões") {
|
||
m.add("Dimensões", format!("{w} × {h} px"));
|
||
}
|
||
}
|
||
}
|
||
if let Some(o) = gps_ptr {
|
||
let (mut lat, mut lon, mut lat_ref, mut lon_ref, mut alt) = (None, None, 'N', 'E', None);
|
||
for e in t.ifd(o as usize) {
|
||
let dms = |e: &Entry| -> Option<f64> {
|
||
Some(t.rational(e, 0)? + t.rational(e, 1)? / 60.0 + t.rational(e, 2)? / 3600.0)
|
||
};
|
||
match e.tag {
|
||
0x0001 => lat_ref = t.ascii(&e).chars().next().unwrap_or('N'),
|
||
0x0002 => lat = dms(&e),
|
||
0x0003 => lon_ref = t.ascii(&e).chars().next().unwrap_or('E'),
|
||
0x0004 => lon = dms(&e),
|
||
0x0006 => alt = t.rational(&e, 0),
|
||
0x001D => m.add("Data do GPS", t.ascii(&e).replace(':', "-")),
|
||
_ => {}
|
||
}
|
||
}
|
||
if let (Some(a), Some(b)) = (lat, lon) {
|
||
let a = if lat_ref == 'S' { -a } else { a };
|
||
let b = if lon_ref == 'W' { -b } else { b };
|
||
if a.abs() <= 90.0 && b.abs() <= 180.0 && !(a == 0.0 && b == 0.0) {
|
||
m.gps = Some((a, b));
|
||
m.add("Localização (GPS)", format!("{a:.6}, {b:.6}"));
|
||
if let Some(h) = alt {
|
||
m.add("Altitude", format!("{h:.0} m"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// "2023:07:14 18:22:05" -> segundos desde 1970 (hora da câmera lida como UTC)
|
||
fn exif_epoch(s: &str) -> Option<i64> {
|
||
let b = s.as_bytes();
|
||
if b.len() < 19 || b[4] != b':' || b[7] != b':' {
|
||
return None;
|
||
}
|
||
let n = |a: usize, c: usize| s.get(a..c)?.parse::<i64>().ok();
|
||
let (y, mo, d, h, mi, se) = (
|
||
n(0, 4)?,
|
||
n(5, 7)?,
|
||
n(8, 10)?,
|
||
n(11, 13)?,
|
||
n(14, 16)?,
|
||
n(17, 19)?,
|
||
);
|
||
if !(1..=12).contains(&mo) || !(1..=31).contains(&d) || y < 1970 {
|
||
return None;
|
||
}
|
||
Some(crate::fs::days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se)
|
||
}
|
||
|
||
/// "2023:07:14 18:22:05" -> "2023-07-14T18:22:05" (a interface formata por idioma)
|
||
fn exif_date(s: &str) -> String {
|
||
let b = s.as_bytes();
|
||
if b.len() >= 19 && b[4] == b':' && b[7] == b':' {
|
||
format!("{}-{}-{}T{}", &s[0..4], &s[5..7], &s[8..10], &s[11..19])
|
||
} else {
|
||
s.to_string()
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------ PDF
|
||
|
||
fn pdf(fr: &Fr, m: &mut Meta) {
|
||
let size = fr.f.size;
|
||
let head = fr.read(0, 96 * 1024);
|
||
let tail = if size > 192 * 1024 {
|
||
fr.read(size - 96 * 1024, 96 * 1024)
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
let mut all = head.clone();
|
||
all.extend_from_slice(&tail);
|
||
if head.starts_with(b"%PDF-") {
|
||
m.add(
|
||
"Versão do PDF",
|
||
String::from_utf8_lossy(&head[5..8.min(head.len())]).to_string(),
|
||
);
|
||
}
|
||
for (key, label) in [
|
||
("/Title", "Título"),
|
||
("/Author", "Autor"),
|
||
("/Subject", "Assunto"),
|
||
("/Creator", "Criado com"),
|
||
("/Producer", "Gerado por"),
|
||
] {
|
||
if let Some(v) = pdf_string(&all, key) {
|
||
m.add(label, v);
|
||
}
|
||
}
|
||
for (key, label) in [
|
||
("/CreationDate", "Criado em"),
|
||
("/ModDate", "Modificado em"),
|
||
] {
|
||
if let Some(v) = pdf_string(&all, key) {
|
||
m.add(label, pdf_date(&v));
|
||
}
|
||
}
|
||
let pages = find_all(&all, b"/Count ").into_iter().filter_map(|i| {
|
||
let s: String = all[i + 7..]
|
||
.iter()
|
||
.take(8)
|
||
.map(|&c| c as char)
|
||
.take_while(|c| c.is_ascii_digit())
|
||
.collect();
|
||
s.parse::<u32>().ok()
|
||
});
|
||
if let Some(n) = pages.max() {
|
||
m.add("Páginas (aprox.)", n.to_string());
|
||
}
|
||
if !find_all(&all, b"/Encrypt").is_empty() {
|
||
m.add("Criptografado", "sim");
|
||
}
|
||
}
|
||
|
||
fn find_all(h: &[u8], n: &[u8]) -> Vec<usize> {
|
||
let mut v = vec![];
|
||
let mut i = 0;
|
||
while i + n.len() <= h.len() {
|
||
if &h[i..i + n.len()] == n {
|
||
v.push(i);
|
||
i += n.len();
|
||
} else {
|
||
i += 1;
|
||
}
|
||
}
|
||
v
|
||
}
|
||
|
||
fn pdf_string(d: &[u8], key: &str) -> Option<String> {
|
||
let k = key.as_bytes();
|
||
for at in find_all(d, k) {
|
||
let mut p = at + k.len();
|
||
// evita /Creator casar com /CreatorXyz
|
||
if d.get(p).map(|c| c.is_ascii_alphanumeric()).unwrap_or(false) {
|
||
continue;
|
||
}
|
||
while d.get(p).map(|c| c.is_ascii_whitespace()).unwrap_or(false) {
|
||
p += 1;
|
||
}
|
||
match d.get(p)? {
|
||
b'(' => {
|
||
let mut depth = 0;
|
||
let mut out = vec![];
|
||
let mut i = p;
|
||
while i < d.len() && out.len() < 400 {
|
||
let c = d[i];
|
||
match c {
|
||
b'\\' if i + 1 < d.len() => {
|
||
i += 1;
|
||
if d[i].is_ascii_digit() && d[i] < b'8' {
|
||
// escape octal \ddd (1 a 3 dígitos)
|
||
let mut v = 0u32;
|
||
let mut n = 0;
|
||
while n < 3 && i < d.len() && d[i] >= b'0' && d[i] < b'8' {
|
||
v = v * 8 + (d[i] - b'0') as u32;
|
||
i += 1;
|
||
n += 1;
|
||
}
|
||
out.push((v & 0xFF) as u8);
|
||
continue;
|
||
}
|
||
out.push(match d[i] {
|
||
b'n' => b'\n',
|
||
b'r' => b'\r',
|
||
b't' => b'\t',
|
||
o => o,
|
||
});
|
||
}
|
||
b'(' => {
|
||
depth += 1;
|
||
if depth > 1 {
|
||
out.push(c);
|
||
}
|
||
}
|
||
b')' => {
|
||
depth -= 1;
|
||
if depth == 0 {
|
||
break;
|
||
}
|
||
out.push(c);
|
||
}
|
||
_ => out.push(c),
|
||
}
|
||
i += 1;
|
||
}
|
||
return Some(pdf_text(&out));
|
||
}
|
||
b'<' if d.get(p + 1) != Some(&b'<') => {
|
||
let hex: Vec<u8> = d[p + 1..]
|
||
.iter()
|
||
.take_while(|&&c| c != b'>')
|
||
.copied()
|
||
.filter(|c| c.is_ascii_hexdigit())
|
||
.collect();
|
||
let bytes: Vec<u8> = hex
|
||
.chunks(2)
|
||
.filter(|c| c.len() == 2)
|
||
.map(|c| u8::from_str_radix(std::str::from_utf8(c).unwrap(), 16).unwrap_or(0))
|
||
.collect();
|
||
return Some(pdf_text(&bytes));
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn pdf_text(b: &[u8]) -> String {
|
||
if b.len() >= 2 && b[0] == 0xFE && b[1] == 0xFF {
|
||
let u: Vec<u16> = b[2..]
|
||
.chunks_exact(2)
|
||
.map(|c| u16::from_be_bytes([c[0], c[1]]))
|
||
.collect();
|
||
return String::from_utf16_lossy(&u);
|
||
}
|
||
// PDFDocEncoding ~ Latin-1 na prática
|
||
b.iter().map(|&c| c as char).collect()
|
||
}
|
||
|
||
/// "D:20230714182205-03'00'" -> "2023-07-14T18:22:05" (a interface formata por idioma)
|
||
fn pdf_date(s: &str) -> String {
|
||
let t = s.trim_start_matches("D:");
|
||
let digits: String = t.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||
if digits.len() >= 8 {
|
||
let g = |a: usize, b: usize| digits.get(a..b).unwrap_or("00");
|
||
return format!(
|
||
"{}-{}-{}T{}:{}:{}",
|
||
g(0, 4),
|
||
g(4, 6),
|
||
g(6, 8),
|
||
g(8, 10),
|
||
g(10, 12),
|
||
g(12, 14)
|
||
);
|
||
}
|
||
s.to_string()
|
||
}
|
||
|
||
// ------------------------------------------------------------------ ZIP (Office/ODF)
|
||
|
||
struct ZipEnt {
|
||
name: String,
|
||
method: u16,
|
||
csize: u64,
|
||
usize: u64,
|
||
off: u64,
|
||
}
|
||
|
||
fn zip_dir(fr: &Fr) -> Vec<ZipEnt> {
|
||
let size = fr.f.size;
|
||
let tail_len = size.min(66_000);
|
||
let tail = fr.read(size - tail_len, tail_len as usize);
|
||
let Some(e) = (0..tail.len().saturating_sub(21))
|
||
.rev()
|
||
.find(|&i| tail[i..].starts_with(b"PK\x05\x06"))
|
||
else {
|
||
return vec![];
|
||
};
|
||
let n = le16(&tail[e + 10..]) as usize;
|
||
let cd_size = le32(&tail[e + 12..]) as usize;
|
||
let cd_off = le32(&tail[e + 16..]) as u64;
|
||
if cd_size > 8 << 20 || cd_off + cd_size as u64 > size {
|
||
return vec![];
|
||
}
|
||
let cd = fr.read(cd_off, cd_size);
|
||
let mut out = vec![];
|
||
let mut p = 0;
|
||
while out.len() < n.min(5000) && p + 46 <= cd.len() && &cd[p..p + 4] == b"PK\x01\x02" {
|
||
let nl = le16(&cd[p + 28..]) as usize;
|
||
let xl = le16(&cd[p + 30..]) as usize;
|
||
let cl = le16(&cd[p + 32..]) as usize;
|
||
if p + 46 + nl > cd.len() {
|
||
break;
|
||
}
|
||
out.push(ZipEnt {
|
||
name: String::from_utf8_lossy(&cd[p + 46..p + 46 + nl]).to_string(),
|
||
method: le16(&cd[p + 10..]),
|
||
csize: le32(&cd[p + 20..]) as u64,
|
||
usize: le32(&cd[p + 24..]) as u64,
|
||
off: le32(&cd[p + 42..]) as u64,
|
||
});
|
||
p += 46 + nl + xl + cl;
|
||
}
|
||
out
|
||
}
|
||
|
||
fn zip_read(fr: &Fr, e: &ZipEnt) -> Option<Vec<u8>> {
|
||
if e.usize > 4 << 20 {
|
||
return None;
|
||
}
|
||
let lh = fr.read(e.off, 30);
|
||
if lh.len() < 30 || &lh[0..4] != b"PK\x03\x04" {
|
||
return None;
|
||
}
|
||
let start = e.off + 30 + le16(&lh[26..]) as u64 + le16(&lh[28..]) as u64;
|
||
let data = fr.read(start, e.csize as usize);
|
||
match e.method {
|
||
0 => Some(data),
|
||
8 => {
|
||
let mut out = Vec::new();
|
||
flate2::read::DeflateDecoder::new(&data[..])
|
||
.take(4 << 20)
|
||
.read_to_end(&mut out)
|
||
.ok()?;
|
||
Some(out)
|
||
}
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// Texto do primeiro elemento `<tag ...>texto</tag>`.
|
||
fn xml_tag(xml: &str, tag: &str) -> Option<String> {
|
||
let open = format!("<{tag}");
|
||
let mut from = 0;
|
||
while let Some(i) = xml[from..].find(&open) {
|
||
let s = from + i + open.len();
|
||
match xml[s..].chars().next() {
|
||
Some('>') | Some(' ') | Some('/') => {
|
||
let gt = xml[s..].find('>')? + s;
|
||
if xml[..gt].ends_with('/') {
|
||
return None;
|
||
}
|
||
let end = xml[gt + 1..].find(&format!("</{tag}>"))? + gt + 1;
|
||
return Some(unescape(&xml[gt + 1..end]));
|
||
}
|
||
_ => from = s,
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn unescape(s: &str) -> String {
|
||
s.replace("<", "<")
|
||
.replace(">", ">")
|
||
.replace(""", "\"")
|
||
.replace("'", "'")
|
||
.replace("&", "&")
|
||
}
|
||
|
||
/// Datas ISO 8601 ficam como estão; a interface as formata no idioma escolhido.
|
||
fn iso_date(s: &str) -> String {
|
||
let b = s.as_bytes();
|
||
if b.len() >= 19 && b[4] == b'-' && b[10] == b'T' {
|
||
let mut out = s[..19].to_string();
|
||
if s.len() == 20 && s.ends_with('Z') {
|
||
out.push('Z');
|
||
}
|
||
out
|
||
} else {
|
||
s.to_string()
|
||
}
|
||
}
|
||
|
||
fn ooxml(fr: &Fr, m: &mut Meta) {
|
||
let ents = zip_dir(fr);
|
||
let get = |n: &str| {
|
||
ents.iter()
|
||
.find(|e| e.name == n)
|
||
.and_then(|e| zip_read(fr, e))
|
||
.map(|b| String::from_utf8_lossy(&b).to_string())
|
||
};
|
||
if let Some(x) = get("docProps/core.xml") {
|
||
for (tag, label) in [
|
||
("dc:title", "Título"),
|
||
("dc:subject", "Assunto"),
|
||
("dc:creator", "Autor"),
|
||
("cp:lastModifiedBy", "Última modificação por"),
|
||
("cp:keywords", "Palavras-chave"),
|
||
] {
|
||
if let Some(v) = xml_tag(&x, tag) {
|
||
m.add(label, v);
|
||
}
|
||
}
|
||
for (tag, label) in [
|
||
("dcterms:created", "Criado em"),
|
||
("dcterms:modified", "Modificado em"),
|
||
] {
|
||
if let Some(v) = xml_tag(&x, tag) {
|
||
m.add(label, iso_date(&v));
|
||
}
|
||
}
|
||
}
|
||
if let Some(x) = get("docProps/app.xml") {
|
||
for (tag, label) in [
|
||
("Application", "Aplicativo"),
|
||
("Company", "Empresa"),
|
||
("Pages", "Páginas"),
|
||
("Words", "Palavras"),
|
||
] {
|
||
if let Some(v) = xml_tag(&x, tag) {
|
||
m.add(label, v);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn odf(fr: &Fr, m: &mut Meta) {
|
||
let ents = zip_dir(fr);
|
||
let Some(x) = ents
|
||
.iter()
|
||
.find(|e| e.name == "meta.xml")
|
||
.and_then(|e| zip_read(fr, e))
|
||
.map(|b| String::from_utf8_lossy(&b).to_string())
|
||
else {
|
||
return;
|
||
};
|
||
for (tag, label) in [
|
||
("dc:title", "Título"),
|
||
("meta:initial-creator", "Criado por"),
|
||
("dc:creator", "Última modificação por"),
|
||
("meta:generator", "Aplicativo"),
|
||
] {
|
||
if let Some(v) = xml_tag(&x, tag) {
|
||
m.add(label, v);
|
||
}
|
||
}
|
||
for (tag, label) in [
|
||
("meta:creation-date", "Criado em"),
|
||
("dc:date", "Modificado em"),
|
||
] {
|
||
if let Some(v) = xml_tag(&x, tag) {
|
||
m.add(label, iso_date(&v));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------ MP4 / MOV
|
||
|
||
fn mp4(fr: &Fr, m: &mut Meta) {
|
||
let size = fr.f.size;
|
||
let mut p = 0u64;
|
||
for _ in 0..64 {
|
||
let h = fr.read(p, 16);
|
||
if h.len() < 8 {
|
||
return;
|
||
}
|
||
let mut sz = be32(&h) as u64;
|
||
if sz == 1 && h.len() >= 16 {
|
||
sz = u64::from_be_bytes(h[8..16].try_into().unwrap());
|
||
} else if sz == 0 {
|
||
sz = size - p;
|
||
}
|
||
if sz < 8 {
|
||
return;
|
||
}
|
||
if &h[4..8] == b"moov" {
|
||
let body = fr.read(p + 8, (sz - 8).min(4 << 20) as usize);
|
||
moov(&body, m);
|
||
return;
|
||
}
|
||
p += sz;
|
||
if p >= size {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
fn boxes(d: &[u8]) -> Vec<(&[u8], &[u8])> {
|
||
let mut v = vec![];
|
||
let mut p = 0;
|
||
while p + 8 <= d.len() {
|
||
let sz = be32(&d[p..]) as usize;
|
||
if sz < 8 || p + sz > d.len() {
|
||
break;
|
||
}
|
||
v.push((&d[p + 4..p + 8], &d[p + 8..p + sz]));
|
||
p += sz;
|
||
}
|
||
v
|
||
}
|
||
|
||
fn moov(d: &[u8], m: &mut Meta) {
|
||
const EPOCH_1904: u64 = 2_082_844_800;
|
||
for (ty, body) in boxes(d) {
|
||
match ty {
|
||
b"mvhd" if body.len() >= 24 => {
|
||
let v = body[0];
|
||
let (created, ts, dur) = if v == 1 && body.len() >= 32 {
|
||
(
|
||
u64::from_be_bytes(body[4..12].try_into().unwrap()),
|
||
be32(&body[20..]) as u64,
|
||
u64::from_be_bytes(body[24..32].try_into().unwrap()),
|
||
)
|
||
} else {
|
||
(
|
||
be32(&body[4..]) as u64,
|
||
be32(&body[12..]) as u64,
|
||
be32(&body[16..]) as u64,
|
||
)
|
||
};
|
||
if created > EPOCH_1904 {
|
||
m.add(
|
||
"Gravado em (metadado)",
|
||
iso_unix((created - EPOCH_1904) as i64),
|
||
);
|
||
}
|
||
if ts > 0 && dur > 0 {
|
||
let s = dur / ts;
|
||
m.add(
|
||
"Duração",
|
||
format!("{}:{:02}:{:02}", s / 3600, s % 3600 / 60, s % 60),
|
||
);
|
||
}
|
||
}
|
||
b"trak" => {
|
||
for (t2, b2) in boxes(body) {
|
||
if t2 == b"tkhd" && b2.len() >= 84 {
|
||
let off = if b2[0] == 1 { 88 } else { 76 };
|
||
if b2.len() >= off + 8 {
|
||
let (w, h) = (be32(&b2[off..]) >> 16, be32(&b2[off + 4..]) >> 16);
|
||
if w > 0 && h > 0 && !m.fields.iter().any(|(k, _)| k == "Dimensões") {
|
||
m.add("Dimensões", format!("{w} × {h} px"));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Instante em ISO 8601 UTC ("2023-07-14T18:22:05Z"), para a interface formatar.
|
||
pub fn iso_unix(t: i64) -> String {
|
||
let days = t.div_euclid(86400);
|
||
let rem = t.rem_euclid(86400);
|
||
let (y, mo, d) = civil_from_days(days);
|
||
format!(
|
||
"{y:04}-{mo:02}-{d:02}T{:02}:{:02}:{:02}Z",
|
||
rem / 3600,
|
||
rem % 3600 / 60,
|
||
rem % 60
|
||
)
|
||
}
|
||
|
||
pub fn fmt_unix(t: i64) -> String {
|
||
let days = t.div_euclid(86400);
|
||
let rem = t.rem_euclid(86400);
|
||
let (y, mo, d) = civil_from_days(days);
|
||
format!(
|
||
"{d:02}/{mo:02}/{y} {:02}:{:02}:{:02} (UTC)",
|
||
rem / 3600,
|
||
rem % 3600 / 60,
|
||
rem % 60
|
||
)
|
||
}
|
||
|
||
/// (ano, mês, dia) de um instante em segundos desde 1970.
|
||
pub fn ymd(t: i64) -> (i64, u32, u32) {
|
||
let (y, m, d) = civil_from_days(t.div_euclid(86400));
|
||
(y, m as u32, d as u32)
|
||
}
|
||
|
||
/// Inverso de `days_from_civil` (Howard Hinnant).
|
||
fn civil_from_days(z: i64) -> (i64, i64, i64) {
|
||
let z = z + 719468;
|
||
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
||
let doe = z - era * 146097;
|
||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||
let y = yoe + era * 400;
|
||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||
let mp = (5 * doy + 2) / 153;
|
||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||
(if m <= 2 { y + 1 } else { y }, m, d)
|
||
}
|