use anyhow::{Context, Result, anyhow, bail};
use std::path::Path;
use std::sync::Mutex;
pub mod exif;
pub mod jpeg;
pub const MIN_PREVIEW_LONG_EDGE: u16 = 1600;
#[derive(Debug)]
pub struct RenderedJpeg {
pub data: Vec<u8>,
}
pub fn rendered_file_name(path: &Path) -> String {
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "image".to_string());
format!("{}.jpg", stem)
}
pub fn render_jpeg(path: &Path) -> Result<RenderedJpeg> {
let data = std::fs::read(path)?;
render_jpeg_bytes(&data)
}
const CONVERTED_JPEG_QUALITY: u8 = 92;
pub fn render_jpeg_bytes(data: &[u8]) -> Result<RenderedJpeg> {
let mut out = match embedded_preview(data) {
Ok(preview) if preview.has_exif => {
return Ok(RenderedJpeg {
data: data[preview.start..preview.end].to_vec(),
});
}
Ok(preview) => data[preview.start..preview.end].to_vec(),
Err(no_preview) => convert_raw(data)
.map_err(|e| anyhow!("{no_preview}, and converting the RAW data failed: {e:#}"))?,
};
if let Some(exif) = exif::read_raw(data).filter(|e| !e.is_empty()) {
let mut with_exif = out.clone();
if exif.write_into_jpeg(&mut with_exif).is_ok() {
out = with_exif;
}
}
Ok(RenderedJpeg { data: out })
}
fn embedded_preview(data: &[u8]) -> Result<jpeg::EmbeddedJpeg> {
let Some(best) = jpeg::find_jpegs(data)
.into_iter()
.reduce(|best, p| if p.pixels() > best.pixels() { p } else { best })
else {
bail!("no embedded JPEG preview found");
};
if best.long_edge() < MIN_PREVIEW_LONG_EDGE {
bail!(
"embedded preview is only {}x{} (need at least {} px on the long edge)",
best.width,
best.height,
MIN_PREVIEW_LONG_EDGE
);
}
Ok(best)
}
fn convert_raw(data: &[u8]) -> Result<Vec<u8>> {
use image::codecs::jpeg::JpegEncoder;
use rawler::decoders::RawDecodeParams;
use rawler::imgop::develop::RawDevelop;
use rawler::rawsource::RawSource;
static CONVERTING: Mutex<()> = Mutex::new(());
let _one_at_a_time = CONVERTING.lock().unwrap_or_else(|e| e.into_inner());
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let source = RawSource::new_from_slice(data);
let raw = rawler::decode(&source, &RawDecodeParams::default())?;
let developed = RawDevelop::default()
.develop_intermediate(&raw)?
.to_dynamic_image()
.context("developed image has an unexpected size")?;
let mut out = Vec::new();
JpegEncoder::new_with_quality(&mut out, CONVERTED_JPEG_QUALITY)
.encode_image(&developed.into_rgb8())?;
Ok(out)
}))
.map_err(|_| anyhow!("the RAW decoder crashed on this file"))?
}
#[cfg(test)]
mod tests {
use super::*;
use crate::raw::exif::tests::fake_tiff_raw_be;
use crate::raw::jpeg::tests::fake_jpeg;
#[test]
fn test_rendered_file_name() {
assert_eq!(
rendered_file_name(Path::new("/a/IMG_1234.CR2")),
"IMG_1234.jpg"
);
assert_eq!(rendered_file_name(Path::new("x.y.nef")), "x.y.jpg");
}
#[test]
fn picks_largest_preview_and_adds_exif() {
let mut raw = fake_tiff_raw_be();
let thumb = fake_jpeg(0xC0, 160, 120, None);
let full = fake_jpeg(0xC0, 6000, 4000, None);
let sensor = fake_jpeg(0xC3, 6100, 4100, None);
raw.extend(&thumb);
raw.extend(&full);
raw.extend(&sensor);
let rendered = render_jpeg_bytes(&raw).unwrap();
assert!(rendered.data.ends_with(&full[2..]));
let found = jpeg::find_jpegs(&rendered.data);
assert_eq!(found.len(), 1);
assert!(found[0].has_exif);
assert_eq!((found[0].width, found[0].height), (6000, 4000));
assert_eq!(found[0].end, rendered.data.len());
let exif = exif::read_tiff_raw(exif::tests::exif_tiff(&rendered.data).unwrap()).unwrap();
assert_eq!(exif.orientation(), Some(6));
assert_eq!(render_jpeg_bytes(&raw).unwrap().data, rendered.data);
}
#[test]
fn keeps_preview_exif_when_present() {
let mut raw = fake_tiff_raw_be();
let full = fake_jpeg(0xC0, 6000, 4000, Some(b"Exif\0\0own"));
raw.extend(&full);
let rendered = render_jpeg_bytes(&raw).unwrap();
assert_eq!(rendered.data, full);
}
#[test]
fn falls_back_to_converting_without_a_usable_preview() {
let mut raw = fake_tiff_raw_be();
let err = render_jpeg_bytes(&raw).unwrap_err().to_string();
assert!(err.contains("no embedded JPEG preview"), "{err}");
assert!(err.contains("converting the RAW data failed"), "{err}");
raw.extend(fake_jpeg(0xC0, 1024, 683, None));
let err = render_jpeg_bytes(&raw).unwrap_err().to_string();
assert!(err.contains("1024x683"), "{err}");
assert!(err.contains("converting the RAW data failed"), "{err}");
}
}