Skip to main content

smugmug_cli/raw/
mod.rs

1//! Turning RAW files into JPEGs that any SmugMug account can take.
2//!
3//! RAW uploads need a SmugMug Source subscription. Without one, the
4//! uploader sends a JPEG instead: the full-size preview the camera embedded
5//! in the RAW file, with the RAW's EXIF (capture date, camera, exposure,
6//! GPS, orientation) copied in when the preview has none of its own. That
7//! is the camera's own rendering (picture style, white balance), without
8//! any edits made in a RAW editor, and is byte-for-byte the same each run.
9//!
10//! When a file has no usable preview (DNGs from Adobe DNG Converter often
11//! carry only a 1024 px one), the RAW data itself is converted with
12//! `rawler` (LGPL-2.1): demosaiced, white balanced and converted to sRGB,
13//! with no tone curve, so it looks flatter than the camera's rendering.
14
15use anyhow::{Context, Result, anyhow, bail};
16use std::path::Path;
17use std::sync::Mutex;
18
19pub mod exif;
20pub mod jpeg;
21
22/// Previews with a shorter long edge than this are thumbnails, not photos,
23/// and aren't uploaded in place of the RAW.
24pub const MIN_PREVIEW_LONG_EDGE: u16 = 1600;
25
26/// A RAW file rendered to JPEG, ready to upload.
27#[derive(Debug)]
28pub struct RenderedJpeg {
29    pub data: Vec<u8>,
30}
31
32/// The file name a RAW file's rendered JPEG is uploaded under:
33/// `IMG_1234.CR2` becomes `IMG_1234.jpg`.
34pub fn rendered_file_name(path: &Path) -> String {
35    let stem = path
36        .file_stem()
37        .map(|s| s.to_string_lossy().into_owned())
38        .unwrap_or_else(|| "image".to_string());
39    format!("{}.jpg", stem)
40}
41
42/// Render the RAW file at `path` to a JPEG (see the module docs).
43pub fn render_jpeg(path: &Path) -> Result<RenderedJpeg> {
44    let data = std::fs::read(path)?;
45    render_jpeg_bytes(&data)
46}
47
48/// JPEG quality for RAW files converted with `rawler`.
49const CONVERTED_JPEG_QUALITY: u8 = 92;
50
51pub fn render_jpeg_bytes(data: &[u8]) -> Result<RenderedJpeg> {
52    let mut out = match embedded_preview(data) {
53        Ok(preview) if preview.has_exif => {
54            return Ok(RenderedJpeg {
55                data: data[preview.start..preview.end].to_vec(),
56            });
57        }
58        Ok(preview) => data[preview.start..preview.end].to_vec(),
59        Err(no_preview) => convert_raw(data)
60            .map_err(|e| anyhow!("{no_preview}, and converting the RAW data failed: {e:#}"))?,
61    };
62
63    // Neither a bare preview nor a converted image has the RAW's EXIF. If it
64    // can't be written, upload without it rather than not at all.
65    if let Some(exif) = exif::read_raw(data).filter(|e| !e.is_empty()) {
66        let mut with_exif = out.clone();
67        if exif.write_into_jpeg(&mut with_exif).is_ok() {
68            out = with_exif;
69        }
70    }
71
72    Ok(RenderedJpeg { data: out })
73}
74
75/// The largest JPEG preview embedded in `data` (the earliest wins a tie),
76/// if it's big enough to stand in for the photo.
77fn embedded_preview(data: &[u8]) -> Result<jpeg::EmbeddedJpeg> {
78    let Some(best) = jpeg::find_jpegs(data)
79        .into_iter()
80        .reduce(|best, p| if p.pixels() > best.pixels() { p } else { best })
81    else {
82        bail!("no embedded JPEG preview found");
83    };
84    if best.long_edge() < MIN_PREVIEW_LONG_EDGE {
85        bail!(
86            "embedded preview is only {}x{} (need at least {} px on the long edge)",
87            best.width,
88            best.height,
89            MIN_PREVIEW_LONG_EDGE
90        );
91    }
92    Ok(best)
93}
94
95/// Decode and develop the RAW data itself with `rawler`, as a JPEG.
96fn convert_raw(data: &[u8]) -> Result<Vec<u8>> {
97    use image::codecs::jpeg::JpegEncoder;
98    use rawler::decoders::RawDecodeParams;
99    use rawler::imgop::develop::RawDevelop;
100    use rawler::rawsource::RawSource;
101
102    // A developed 45 MP image takes well over a gigabyte in floating point,
103    // so conversions run one at a time however many upload workers there are.
104    static CONVERTING: Mutex<()> = Mutex::new(());
105    let _one_at_a_time = CONVERTING.lock().unwrap_or_else(|e| e.into_inner());
106
107    // Decoders for unusual files can panic; that's a failed file, not a
108    // failed upload run.
109    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
110        let source = RawSource::new_from_slice(data);
111        let raw = rawler::decode(&source, &RawDecodeParams::default())?;
112        let developed = RawDevelop::default()
113            .develop_intermediate(&raw)?
114            .to_dynamic_image()
115            .context("developed image has an unexpected size")?;
116
117        let mut out = Vec::new();
118        JpegEncoder::new_with_quality(&mut out, CONVERTED_JPEG_QUALITY)
119            .encode_image(&developed.into_rgb8())?;
120        Ok(out)
121    }))
122    .map_err(|_| anyhow!("the RAW decoder crashed on this file"))?
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::raw::exif::tests::fake_tiff_raw_be;
129    use crate::raw::jpeg::tests::fake_jpeg;
130
131    #[test]
132    fn test_rendered_file_name() {
133        assert_eq!(
134            rendered_file_name(Path::new("/a/IMG_1234.CR2")),
135            "IMG_1234.jpg"
136        );
137        assert_eq!(rendered_file_name(Path::new("x.y.nef")), "x.y.jpg");
138    }
139
140    #[test]
141    fn picks_largest_preview_and_adds_exif() {
142        let mut raw = fake_tiff_raw_be();
143        let thumb = fake_jpeg(0xC0, 160, 120, None);
144        let full = fake_jpeg(0xC0, 6000, 4000, None);
145        let sensor = fake_jpeg(0xC3, 6100, 4100, None);
146        raw.extend(&thumb);
147        raw.extend(&full);
148        raw.extend(&sensor);
149
150        let rendered = render_jpeg_bytes(&raw).unwrap();
151
152        // The result is the preview with an EXIF segment added, and it
153        // parses as exactly one JPEG of the same size.
154        assert!(rendered.data.ends_with(&full[2..]));
155        let found = jpeg::find_jpegs(&rendered.data);
156        assert_eq!(found.len(), 1);
157        assert!(found[0].has_exif);
158        assert_eq!((found[0].width, found[0].height), (6000, 4000));
159        assert_eq!(found[0].end, rendered.data.len());
160
161        let exif = exif::read_tiff_raw(exif::tests::exif_tiff(&rendered.data).unwrap()).unwrap();
162        assert_eq!(exif.orientation(), Some(6));
163
164        // Deterministic, so re-runs produce identical bytes.
165        assert_eq!(render_jpeg_bytes(&raw).unwrap().data, rendered.data);
166    }
167
168    #[test]
169    fn keeps_preview_exif_when_present() {
170        let mut raw = fake_tiff_raw_be();
171        let full = fake_jpeg(0xC0, 6000, 4000, Some(b"Exif\0\0own"));
172        raw.extend(&full);
173
174        let rendered = render_jpeg_bytes(&raw).unwrap();
175        assert_eq!(rendered.data, full);
176    }
177
178    #[test]
179    fn falls_back_to_converting_without_a_usable_preview() {
180        // These fakes have no real RAW data either, so conversion fails too,
181        // and the error says why neither worked.
182        let mut raw = fake_tiff_raw_be();
183        let err = render_jpeg_bytes(&raw).unwrap_err().to_string();
184        assert!(err.contains("no embedded JPEG preview"), "{err}");
185        assert!(err.contains("converting the RAW data failed"), "{err}");
186
187        raw.extend(fake_jpeg(0xC0, 1024, 683, None));
188        let err = render_jpeg_bytes(&raw).unwrap_err().to_string();
189        assert!(err.contains("1024x683"), "{err}");
190        assert!(err.contains("converting the RAW data failed"), "{err}");
191    }
192}