use crate::generate_error::{ProofModeError, Result};
use crate::generate_types::ProofData;
use c2pa::{Builder, BuilderIntent, ClaimGeneratorInfo, EphemeralSigner};
use std::collections::HashMap;
use std::io::Cursor;
pub fn detect_mime_type(media_data: &[u8], metadata: &HashMap<String, String>) -> String {
if let Some(mime) = metadata.get("mime_type") {
if !mime.is_empty() {
return mime.clone();
}
}
if let Some(file_name) = metadata.get("fileName") {
let guess = mime_guess::from_path(file_name);
if let Some(mime) = guess.first() {
return mime.to_string();
}
}
detect_mime_from_bytes(media_data)
}
fn detect_mime_from_bytes(data: &[u8]) -> String {
if data.len() >= 4 {
if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF {
return "image/jpeg".to_string();
}
if data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 {
return "image/png".to_string();
}
if data.len() >= 12
&& data[0] == 0x52
&& data[1] == 0x49
&& data[2] == 0x46
&& data[3] == 0x46
&& data[8] == 0x57
&& data[9] == 0x45
&& data[10] == 0x42
&& data[11] == 0x50
{
return "image/webp".to_string();
}
}
if data.len() >= 12 {
if data[4] == 0x66 && data[5] == 0x74 && data[6] == 0x79 && data[7] == 0x70 {
if data[8] == 0x68 && data[9] == 0x65 && data[10] == 0x69 && data[11] == 0x63 {
return "image/heic".to_string();
}
return "video/mp4".to_string();
}
}
"image/jpeg".to_string()
}
fn extension_for_mime(mime_type: &str) -> &str {
match mime_type {
"image/jpeg" => "jpg",
"image/png" => "png",
"image/heic" => "heic",
"image/heif" => "heif",
"image/webp" => "webp",
"image/avif" => "avif",
"image/tiff" => "tiff",
"video/mp4" => "mp4",
"video/quicktime" => "mov",
_ => "jpg",
}
}
pub fn embed_c2pa_manifest(
media_data: &[u8],
mime_type: &str,
proof_data: &ProofData,
) -> Result<Vec<u8>> {
let mut builder = Builder::new();
builder.set_intent(BuilderIntent::Create(
c2pa::assertions::DigitalSourceType::DigitalCapture,
));
let mut cgi = ClaimGeneratorInfo::new("ProofMode");
cgi.set_version(env!("CARGO_PKG_VERSION"));
builder.set_claim_generator_info(cgi);
let proof_json = serde_json::to_value(proof_data)
.map_err(|e| ProofModeError::Serialization(e.to_string()))?;
builder
.add_assertion_json("org.proofmode.metadata", &proof_json)
.map_err(|e| ProofModeError::Crypto(format!("C2PA add assertion failed: {}", e)))?;
let signer = EphemeralSigner::new("proofmode.local")
.map_err(|e| ProofModeError::Crypto(format!("C2PA signer creation failed: {}", e)))?;
let mut source = Cursor::new(media_data);
let mut dest = Cursor::new(Vec::new());
builder
.sign(&signer, mime_type, &mut source, &mut dest)
.map_err(|e| ProofModeError::Crypto(format!("C2PA signing failed: {}", e)))?;
Ok(dest.into_inner())
}
pub fn embed_and_save_c2pa<C: crate::generate::core::PlatformCallbacks>(
media_data: &[u8],
hash: &str,
proof_data: &ProofData,
metadata: &HashMap<String, String>,
callbacks: &C,
) -> Result<String> {
let mime_type = detect_mime_type(media_data, metadata);
let embedded = embed_c2pa_manifest(media_data, &mime_type, proof_data)?;
let ext = extension_for_mime(&mime_type);
let filename = format!("{}{}.{}", hash, crate::generate_types::C2PA_FILE_TAG, ext);
callbacks.save_data(hash, &filename, &embedded)?;
Ok(ext.to_string())
}