mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
#1065: import DNG stills and CinemaDNG folders as one clip
Problem: .dng files were not importable, and a DNG (a TIFF) would have gone to the TIFF decoder, which reads at best the embedded preview. A CinemaDNG shot (a folder of numbered DNG frames) could not be edited. Fix: "dng" joins the importable still extensions. filmcraft-media develops DNGs with filmcraft-dng into scene-linear Rec. 709 float frames (transfer Linear, like OpenEXR), so highlights above 1.0 reach the compositor. A single DNG is a still; a numbered folder imports through the existing image-sequence path as one clip, whose frame rate and start time code come from the first frame's CinemaDNG FrameRate and TimeCodes tags (else Settings > Media > Indeterminate Media Timebase). Lossy and JPEG XL DNGs are refused as unsupported by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
28247afae7
commit
dd7a16788e
@@ -46,4 +46,5 @@ web-time = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-testkit = { workspace = true }
|
||||
filmcraft-dng = { workspace = true, features = ["writer"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
|
||||
@@ -138,6 +138,71 @@ fn import_image_sequences_setting_detects_a_numbered_still() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A CinemaDNG frame: 24 × 16 RGGB, 12-bit lossless JPEG behind a preview, a flat grey at `level`
|
||||
/// (white balanced by the as-shot neutral), 23.976 fps, time code 01:00:00:00 + `frame`.
|
||||
fn cinemadng_frame(level: f64, frame: u8) -> Vec<u8> {
|
||||
use filmcraft_dng::tiff::tags;
|
||||
use filmcraft_dng::writer::{Compression, Options, Value, write};
|
||||
let (w, h) = (24, 16);
|
||||
let neutral = [0.6, 1.0, 0.7];
|
||||
let px: Vec<u16> =
|
||||
(0..w * h).map(|i| (level * [neutral[0], neutral[1], neutral[1], neutral[2]][(i / w % 2) * 2 + i % 2] * 4000.0 + 95.0).round() as u16).collect();
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
bits: 12,
|
||||
preview: true,
|
||||
compression: Compression::LosslessJpeg { predictor: 1, components: 2, restart_lines: 0, pt: 0 },
|
||||
raw_tags: vec![(tags::BLACK_LEVEL, Value::Short(vec![95])), (tags::WHITE_LEVEL, Value::Short(vec![4095]))],
|
||||
main_tags: vec![
|
||||
(tags::AS_SHOT_NEUTRAL, Value::rationals(&neutral)),
|
||||
(tags::FRAME_RATE, Value::SRational(vec![(24_000, 1001)])),
|
||||
(tags::TIME_CODES, Value::Byte(vec![frame, 0, 0, 0x01, 0, 0, 0, 0])),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
write(&px, &o)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_cinemadng_folder_as_one_clip() {
|
||||
let dir = tmp_dir("cinemadng-import");
|
||||
let mut s = demo();
|
||||
s.execute("prefs.set", json!({"key": "media.indeterminateTimebase", "value": "25"})).unwrap();
|
||||
let paths: Vec<String> = (0..4u8)
|
||||
.map(|i| {
|
||||
let p = dir.join(format!("A001_C002_0101AB_{:06}.dng", i));
|
||||
std::fs::write(&p, cinemadng_frame(0.1 + 0.2 * f64::from(i), i)).unwrap();
|
||||
p.to_string_lossy().to_string()
|
||||
})
|
||||
.collect();
|
||||
let r = s.execute("file.import", json!({"paths": [paths[0]], "imageSequence": true})).unwrap();
|
||||
let id = item_of(&r);
|
||||
assert_eq!(r["imageSequences"][0]["frames"], 4);
|
||||
let m = s.project.item(id).unwrap().as_media().unwrap().clone();
|
||||
assert_eq!(m.info.kind, MediaKind::ImageSequence);
|
||||
assert_eq!(m.info.frame_rate(), FrameRate::FPS_23_976, "the CinemaDNG FrameRate tag, not the timebase setting");
|
||||
assert_eq!(m.info.duration, FrameRate::FPS_23_976.tick_of(4));
|
||||
assert_eq!(m.info.start_timecode, Some(3600 * 24), "01:00:00:00 at 24 nominal");
|
||||
let v = m.info.video.as_ref().unwrap();
|
||||
assert_eq!((v.width, v.height), (24, 16));
|
||||
let src = s.media.source_for(&s.project, id, &*s.services).unwrap();
|
||||
for i in 0..4 {
|
||||
let f = src.video_frame(FrameRequest::full(FrameRate::FPS_23_976.tick_of(i))).unwrap();
|
||||
let filmcraft_frame::PixelData::RgbaF32(px) = &f.data else { panic!("linear float frames") };
|
||||
let want = 0.1 + 0.2 * i as f32;
|
||||
for k in 0..3 {
|
||||
assert!((px[(8 * 24 + 8) * 4 + k] - want).abs() < 2e-3, "frame {i} channel {k}: {}", px[(8 * 24 + 8) * 4 + k]);
|
||||
}
|
||||
}
|
||||
// one frame on its own is a still
|
||||
let r = s.execute("file.import", json!({"paths": [paths[3]]})).unwrap();
|
||||
let still = s.project.item(item_of(&r)).unwrap().as_media().unwrap();
|
||||
assert_eq!(still.info.kind, MediaKind::Still);
|
||||
assert_eq!(still.info.video.as_ref().unwrap().codec, "DNG");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sequence_survives_save_and_reopen() {
|
||||
let dir = tmp_dir("imgseq-reopen");
|
||||
|
||||
@@ -13,7 +13,11 @@ workspace = true
|
||||
filmcraft-time = { workspace = true }
|
||||
filmcraft-color = { workspace = true }
|
||||
filmcraft-frame = { workspace = true }
|
||||
filmcraft-dng = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "tiff", "bmp"] }
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-dng = { workspace = true, features = ["writer"] }
|
||||
|
||||
@@ -171,7 +171,7 @@ pub trait MediaSource: Send + Sync {
|
||||
pub type SharedSource = Arc<dyn MediaSource>;
|
||||
|
||||
/// File extensions we recognise at import (the media browser filters on these).
|
||||
pub const STILL_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "tga", "dpx"];
|
||||
pub const STILL_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "tif", "tiff", "bmp", "tga", "dpx", "dng"];
|
||||
pub const AUDIO_EXTENSIONS: &[&str] = &["wav", "wave", "bwf", "aif", "aiff", "mp3", "mp2", "m4a", "aac", "flac", "ogg", "opus"];
|
||||
pub const VIDEO_EXTENSIONS: &[&str] = &[
|
||||
"mp4", "m4v", "mov", "mkv", "webm", "avi", "mxf", "mts", "m2ts", "m2t", "ts", "mpg", "mpeg", "vob", "mod", "tod", "m2v", "m1v", "mpv", "3gp", "y4m", "apv",
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
//! - [`Numbered`] splits a file name around its last run of digits; [`sequence_frames`] lists the
|
||||
//! frames from the chosen file's number up to the highest number present (gaps are `None`).
|
||||
//! - [`ImageSequenceSource`] decodes frames on demand through a [`FrameLoader`] (the host's file
|
||||
//! access) with the still decoders (PNG, JPEG, TIFF, BMP, WebP, GIF, Targa, DPX), keeps recently used frames
|
||||
//! in a byte-budgeted [`FrameCache`], and shows the nearest earlier frame for a missing or
|
||||
//! undecodable one (the nearest later frame before the first).
|
||||
//! - The frame rate is the caller's (Settings ▸ Media ▸ Indeterminate Media Timebase).
|
||||
//! access) with the still decoders (PNG, JPEG, TIFF, BMP, WebP, GIF, Targa, DPX; DNG camera raw
|
||||
//! as linear float frames), keeps recently used frames in a byte-budgeted [`FrameCache`], and
|
||||
//! shows the nearest earlier frame for a missing or undecodable one (the nearest later frame
|
||||
//! before the first).
|
||||
//! - The frame rate is the caller's (Settings ▸ Media ▸ Indeterminate Media Timebase), except for
|
||||
//! CinemaDNG: a frame rate and start time code in the first frame's tags are the sequence's.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -134,8 +136,12 @@ impl ImageSequenceSource {
|
||||
let bytes = loader(&first).map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound { MediaError::Offline(format!("{first}: {e}")) } else { MediaError::Io(format!("{first}: {e}")) }
|
||||
})?;
|
||||
let img = crate::still::decode_raster(&first, &bytes)?;
|
||||
let (w, h, has_alpha) = (img.width, img.height, img.has_alpha);
|
||||
let (frame, has_alpha, pixel_format, timing) = decode_frame(&first, &bytes)?;
|
||||
let (w, h) = (frame.width, frame.height);
|
||||
let (rate, start_timecode) = match timing {
|
||||
Some(info) => crate::still::dng::sequence_timing(&info, rate),
|
||||
None => (rate, None),
|
||||
};
|
||||
let ext = first.rsplit('.').next().unwrap_or("image").to_ascii_uppercase();
|
||||
let n = frames.len() as i64;
|
||||
let info = MediaInfo {
|
||||
@@ -148,19 +154,19 @@ impl ImageSequenceSource {
|
||||
frame_rate: rate,
|
||||
par: (1, 1),
|
||||
codec: format!("{ext} sequence"),
|
||||
pixel_format: if has_alpha { "RGBA 8-bit".into() } else { "RGB 8-bit".into() },
|
||||
color: filmcraft_color::ColorInfo::SRGB_FULL,
|
||||
pixel_format,
|
||||
color: frame.color,
|
||||
has_alpha,
|
||||
bitrate: None,
|
||||
hdr: None,
|
||||
}),
|
||||
audio_streams: Vec::new(),
|
||||
container: "Image Sequence".into(),
|
||||
start_timecode: None,
|
||||
start_timecode,
|
||||
file_size: None,
|
||||
};
|
||||
let cache = FrameCache::new(256 << 20);
|
||||
cache.insert(frames.iter().position(Option::is_some).unwrap_or(0), Arc::new(VideoFrame::rgba8(w, h, img.rgba)));
|
||||
cache.insert(frames.iter().position(Option::is_some).unwrap_or(0), Arc::new(frame));
|
||||
Ok(Self { info, frames, loader, cache })
|
||||
}
|
||||
|
||||
@@ -184,8 +190,7 @@ impl ImageSequenceSource {
|
||||
}
|
||||
let path = self.frames.get(i).and_then(|f| f.as_deref()).ok_or(MediaError::NoStream("video"))?;
|
||||
let bytes = (self.loader)(path).map_err(|e| MediaError::Io(format!("{path}: {e}")))?;
|
||||
let img = crate::still::decode_raster(path, &bytes)?;
|
||||
let f = Arc::new(VideoFrame::rgba8(img.width, img.height, img.rgba));
|
||||
let f = Arc::new(decode_frame(path, &bytes)?.0);
|
||||
self.cache.insert(i, f.clone());
|
||||
Ok(f)
|
||||
}
|
||||
@@ -216,6 +221,19 @@ impl MediaSource for ImageSequenceSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one frame file: DNG camera raw as linear floats (with its metadata, for the sequence's
|
||||
/// timing), every other format through the still decoders as RGBA8. Returns the frame, whether it
|
||||
/// has alpha, and its pixel format for the media info.
|
||||
fn decode_frame(name: &str, bytes: &[u8]) -> Result<(VideoFrame, bool, String, Option<filmcraft_dng::Info>)> {
|
||||
if crate::still::dng::sniff(bytes) {
|
||||
let d = crate::still::dng::decode(name, bytes)?;
|
||||
return Ok((d.frame, false, d.pixel_format, Some(d.info)));
|
||||
}
|
||||
let img = crate::still::decode_raster(name, bytes)?;
|
||||
let pixel_format = if img.has_alpha { "RGBA 8-bit" } else { "RGB 8-bit" }.to_string();
|
||||
Ok((VideoFrame::rgba8(img.width, img.height, img.rgba), img.has_alpha, pixel_format, None))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Still images (PNG, JPEG, GIF (first frame), WebP, TIFF, BMP, Targa, DPX) as media sources.
|
||||
//! Still images (PNG, JPEG, GIF (first frame), WebP, TIFF, BMP, Targa, DPX, DNG camera raw) as media
|
||||
//! sources.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -7,6 +8,8 @@ use filmcraft_time::{FrameRate, TICKS_PER_SECOND, Tick};
|
||||
|
||||
use crate::{FrameRequest, MediaError, MediaInfo, MediaKind, MediaSource, Result, VideoStreamInfo};
|
||||
|
||||
pub mod dng;
|
||||
|
||||
/// Default still duration (Premiere's preference default is 5 seconds).
|
||||
pub const DEFAULT_STILL_DURATION: Tick = Tick(5 * TICKS_PER_SECOND);
|
||||
|
||||
@@ -55,6 +58,10 @@ pub struct StillSource {
|
||||
|
||||
impl StillSource {
|
||||
pub fn decode(name: &str, bytes: &[u8]) -> Result<Self> {
|
||||
// a DNG is a TIFF too: develop its raw image instead of reading its preview
|
||||
if dng::sniff(bytes) {
|
||||
return dng::still(name, bytes);
|
||||
}
|
||||
let r = decode_raster(name, bytes)?;
|
||||
Ok(Self::from_rgba(name, r.width, r.height, r.rgba, r.has_alpha, bytes.len() as u64))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
//! DNG camera raw stills and CinemaDNG frames (decoded by `filmcraft-dng`).
|
||||
//!
|
||||
//! The raw image is developed the way the DNG specification describes (linearisation, black and
|
||||
//! white levels, the camera's as-shot white balance, demosaic, default crop, colour matrices and
|
||||
//! baseline exposure) into scene-linear Rec. 709, kept as 32-bit floats: the compositor's working
|
||||
//! format, so highlights above 1.0 survive into effects and colour work, exactly as with OpenEXR.
|
||||
//! A white the sensor clipped is 1.0 × 2^BaselineExposure.
|
||||
//!
|
||||
//! A numbered folder of `.dng` frames imports as an image sequence (CinemaDNG); the first frame's
|
||||
//! CinemaDNG FrameRate and TimeCodes tags give the sequence its frame rate and start time code.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use filmcraft_color::{ColorInfo, Transfer};
|
||||
use filmcraft_frame::VideoFrame;
|
||||
use filmcraft_time::FrameRate;
|
||||
|
||||
use super::{DEFAULT_STILL_DURATION, StillSource};
|
||||
use crate::{MediaError, MediaInfo, MediaKind, Result, VideoStreamInfo};
|
||||
|
||||
/// Whether `b` is a DNG file.
|
||||
pub fn sniff(b: &[u8]) -> bool {
|
||||
filmcraft_dng::sniff(b)
|
||||
}
|
||||
|
||||
/// The colour of developed frames: scene-linear light, Rec. 709 primaries.
|
||||
const LINEAR_709: ColorInfo = ColorInfo { transfer: Transfer::Linear, ..ColorInfo::SRGB_FULL };
|
||||
|
||||
/// A developed DNG frame.
|
||||
pub struct Decoded {
|
||||
/// Linear Rec. 709 RGBA floats (opaque), default-crop sized.
|
||||
pub frame: VideoFrame,
|
||||
/// For the media info, e.g. "Bayer 12-bit raw (lossless JPEG)".
|
||||
pub pixel_format: String,
|
||||
pub info: filmcraft_dng::Info,
|
||||
}
|
||||
|
||||
fn error(name: &str, e: filmcraft_dng::Error) -> MediaError {
|
||||
match e {
|
||||
filmcraft_dng::Error::Unsupported(m) => MediaError::Unsupported(format!("{name}: {m}")),
|
||||
e => MediaError::Decode(format!("{name}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Develop a DNG file.
|
||||
pub fn decode(name: &str, bytes: &[u8]) -> Result<Decoded> {
|
||||
let img = filmcraft_dng::decode(bytes).map_err(|e| error(name, e))?;
|
||||
let (w, h) = (u32::try_from(img.width), u32::try_from(img.height));
|
||||
let (Ok(w), Ok(h)) = (w, h) else { return Err(MediaError::Decode(format!("{name}: picture too large"))) };
|
||||
let i = &img.info;
|
||||
let kind = if i.cfa { "Bayer" } else { "linear" };
|
||||
let pixel_format = format!("{kind} {}-bit raw ({})", i.bits, i.compression);
|
||||
let mut frame = VideoFrame::rgba_f32(w, h, img.rgba);
|
||||
frame.color = LINEAR_709;
|
||||
Ok(Decoded { frame, pixel_format, info: img.info })
|
||||
}
|
||||
|
||||
/// Open a DNG file as a still.
|
||||
pub fn still(name: &str, bytes: &[u8]) -> Result<StillSource> {
|
||||
let d = decode(name, bytes)?;
|
||||
let info = MediaInfo {
|
||||
name: name.to_string(),
|
||||
kind: MediaKind::Still,
|
||||
duration: DEFAULT_STILL_DURATION,
|
||||
video: Some(VideoStreamInfo {
|
||||
width: d.frame.width,
|
||||
height: d.frame.height,
|
||||
frame_rate: FrameRate::FPS_29_97,
|
||||
par: (1, 1),
|
||||
codec: "DNG".into(),
|
||||
pixel_format: d.pixel_format,
|
||||
color: LINEAR_709,
|
||||
has_alpha: false,
|
||||
bitrate: None,
|
||||
hdr: None,
|
||||
}),
|
||||
audio_streams: Vec::new(),
|
||||
container: "Image".into(),
|
||||
start_timecode: None,
|
||||
file_size: Some(bytes.len() as u64),
|
||||
};
|
||||
Ok(StillSource { info, frame: Arc::new(d.frame) })
|
||||
}
|
||||
|
||||
/// The frame rate and start time code (in frames at that rate) of a CinemaDNG sequence whose first
|
||||
/// frame is `info`: the FrameRate tag when present (else `default`), the TimeCodes tag when present.
|
||||
pub fn sequence_timing(info: &filmcraft_dng::Info, default: FrameRate) -> (FrameRate, Option<i64>) {
|
||||
let rate = info.frame_rate.map(|(n, d)| FrameRate::new(n, d).sane()).unwrap_or(default);
|
||||
let tc = info.timecode.map(|t| {
|
||||
let drop = t.drop_frame && rate.supports_drop_frame();
|
||||
filmcraft_time::fields_to_frames(i64::from(t.hours), i64::from(t.minutes), i64::from(t.seconds), i64::from(t.frames), rate, drop)
|
||||
});
|
||||
(rate, tc)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use filmcraft_dng::tiff::tags;
|
||||
use filmcraft_dng::writer::{Compression, Options, Value, write};
|
||||
use filmcraft_frame::PixelData;
|
||||
|
||||
use super::*;
|
||||
use crate::sequence::{FrameLoader, ImageSequenceSource};
|
||||
use crate::{FrameRequest, MediaSource};
|
||||
|
||||
/// A 16 × 12 RGGB DNG of a flat grey at `level` (0–1 of the 12-bit range above black 0), white
|
||||
/// balanced by neutral (0.5, 1, 0.5); no colour matrix, so the camera values are taken as Rec. 709.
|
||||
fn grey_dng(level: f64, extra: Vec<(u16, Value)>) -> Vec<u8> {
|
||||
let (w, h) = (16, 12);
|
||||
let neutral = [0.5, 1.0, 0.5];
|
||||
let pat = [0usize, 1, 1, 2];
|
||||
let px: Vec<u16> = (0..w * h).map(|i| (level * neutral[pat[(i / w % 2) * 2 + i % w % 2]] * 4095.0).round() as u16).collect();
|
||||
let mut main = vec![(tags::AS_SHOT_NEUTRAL, Value::rationals(&neutral))];
|
||||
main.extend(extra);
|
||||
let o = Options {
|
||||
width: w,
|
||||
height: h,
|
||||
bits: 12,
|
||||
compression: Compression::LosslessJpeg { predictor: 1, components: 2, restart_lines: 0, pt: 0 },
|
||||
raw_tags: vec![(tags::WHITE_LEVEL, Value::Short(vec![4095]))],
|
||||
main_tags: main,
|
||||
preview: true,
|
||||
..Default::default()
|
||||
};
|
||||
write(&px, &o)
|
||||
}
|
||||
|
||||
fn red(f: &VideoFrame) -> f32 {
|
||||
match &f.data {
|
||||
PixelData::RgbaF32(p) => p[(5 * 16 + 5) * 4],
|
||||
_ => panic!("float frames"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dng_still_opens_as_linear_float() {
|
||||
let f = grey_dng(0.5, vec![]);
|
||||
// a DNG is a TIFF, but the TIFF decoder must not get it
|
||||
let src = crate::open_bytes("A001_C001_0001.dng", f.clone().into(), &[]).unwrap();
|
||||
let info = src.info();
|
||||
assert_eq!(info.kind, MediaKind::Still);
|
||||
let v = info.video.as_ref().unwrap();
|
||||
assert_eq!((v.width, v.height, v.codec.as_str(), v.has_alpha), (16, 12, "DNG", false));
|
||||
assert_eq!(v.pixel_format, "Bayer 12-bit raw (lossless JPEG)");
|
||||
assert_eq!(v.color.transfer, Transfer::Linear);
|
||||
let frame = src.video_frame(FrameRequest::full(filmcraft_time::Tick(0))).unwrap();
|
||||
assert_eq!(frame.color.transfer, Transfer::Linear);
|
||||
assert!((red(&frame) - 0.5).abs() < 1e-3, "{}", red(&frame));
|
||||
assert!(crate::STILL_EXTENSIONS.contains(&"dng"));
|
||||
assert!(crate::is_importable(std::path::Path::new("/cards/A001/frame_000001.DNG")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn broken_dngs_are_errors() {
|
||||
let f = grey_dng(0.5, vec![]);
|
||||
for n in [8, 64, f.len() / 2, f.len() - 3] {
|
||||
assert!(crate::open_bytes("x.dng", f[..n].to_vec().into(), &[]).is_err(), "{n}");
|
||||
}
|
||||
// lossy DNG is named as unsupported
|
||||
let mut lossy = grey_dng(0.5, vec![]);
|
||||
let at = lossy.windows(4).position(|w| w == [0x03, 0x01, 0x03, 0x00]).unwrap(); // tag 259 SHORT in the raw IFD
|
||||
lossy[at + 8..at + 10].copy_from_slice(&34892u16.to_le_bytes());
|
||||
let e = crate::open_bytes("x.dng", lossy.into(), &[]).err().unwrap();
|
||||
assert!(matches!(e, MediaError::Unsupported(ref m) if m.contains("lossy JPEG")), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cinemadng_sequence_takes_rate_and_time_code_from_the_frames() {
|
||||
let tags = || {
|
||||
vec![
|
||||
(tags::FRAME_RATE, Value::SRational(vec![(24, 1)])),
|
||||
// 10:00:00:12
|
||||
(tags::TIME_CODES, Value::Byte(vec![0x12, 0x00, 0x00, 0x10, 0, 0, 0, 0])),
|
||||
]
|
||||
};
|
||||
let files: HashMap<String, Vec<u8>> = (0..3).map(|i| (format!("/c/A_{i:04}.dng"), grey_dng(0.2 + 0.2 * f64::from(i), tags()))).collect();
|
||||
let loader: FrameLoader =
|
||||
Arc::new(move |p: &str| files.get(p).cloned().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, p.to_string())));
|
||||
let frames = (0..3).map(|i| Some(format!("/c/A_{i:04}.dng"))).collect();
|
||||
// the caller's (Indeterminate Media Timebase) rate is 25; the frames say 24
|
||||
let s = ImageSequenceSource::new("A_0000.dng", frames, FrameRate::FPS_25, loader).unwrap();
|
||||
let info = s.info();
|
||||
assert_eq!(info.kind, MediaKind::ImageSequence);
|
||||
assert_eq!(info.frame_rate(), FrameRate::FPS_24);
|
||||
assert_eq!(info.duration, FrameRate::FPS_24.tick_of(3));
|
||||
assert_eq!(info.start_timecode, Some(10 * 3600 * 24 + 12));
|
||||
let v = info.video.as_ref().unwrap();
|
||||
assert_eq!((v.codec.as_str(), v.color.transfer), ("DNG sequence", Transfer::Linear));
|
||||
for i in 0..3 {
|
||||
let f = s.video_frame(FrameRequest::full(FrameRate::FPS_24.tick_of(i))).unwrap();
|
||||
let want = 0.2 + 0.2 * i as f32;
|
||||
assert!((red(&f) - want).abs() < 1e-3, "frame {i}: {}", red(&f));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timing_without_cinemadng_tags_is_the_callers() {
|
||||
let info = filmcraft_dng::probe(&grey_dng(0.5, vec![])).unwrap();
|
||||
assert_eq!(sequence_timing(&info, FrameRate::FPS_25), (FrameRate::FPS_25, None));
|
||||
// drop frame only where the rate has it
|
||||
let mut i = info.clone();
|
||||
i.frame_rate = Some((30_000, 1001));
|
||||
i.timecode = Some(filmcraft_dng::Timecode { hours: 0, minutes: 1, seconds: 0, frames: 2, drop_frame: true });
|
||||
assert_eq!(sequence_timing(&i, FrameRate::FPS_25), (FrameRate::FPS_29_97, Some(1800)));
|
||||
i.frame_rate = Some((25, 1));
|
||||
assert_eq!(sequence_timing(&i, FrameRate::FPS_24), (FrameRate::FPS_25, Some(1502)));
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,8 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed
|
||||
| DV / DVCPRO / DV100 | decode + encode | none | none | — | 0% | 15–25 h |
|
||||
| MPEG-4 Part 2 | decode | none | none | — | 0% | 10–20 h |
|
||||
| WMV / VC-1 | decode, encode on Windows | none | none | — | 0% | 20–35 h |
|
||||
| Camera RAW: R3D (incl. R3D NE), ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW (plug-in), Cinema DNG | decode (vendor SDKs) | none (#383) | — | — | 0% | 60–100 h for the ones with public specs; vendor-SDK-only formats need an owner decision |
|
||||
| CinemaDNG / DNG | decode | own decoder (#1065): uncompressed and lossless JPEG raw, Bayer and LinearRaw, DNG colour model to linear Rec. 709 floats; folders of `.dng` frames as one clip with the CinemaDNG frame rate and time code; **no** lossy / JPEG XL DNG, MXF-wrapped CinemaDNG, opcode lists or Raw settings UI | — | raw samples bit-exact (and identical to LibRaw's on our synthetic files), processing against hand-computed values | 60% | 10–20 h |
|
||||
| Camera RAW: R3D (incl. R3D NE), ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW (plug-in) | decode (vendor SDKs) | none (#383) | — | — | 0% | 50–90 h for the ones with public specs; vendor-SDK-only formats need an owner decision |
|
||||
| GIF (animated) | encode | decode as still | encode | — | 80% | — |
|
||||
| Uncompressed / v210 / RGB | decode + encode (AVI, MOV) | partial (MOV PCM-style video not verified) | none | — | 30% | 8–15 h |
|
||||
|
||||
@@ -60,4 +61,5 @@ contents). Percent is ready-for-real-work for that codec: depth, profiles, speed
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-11 | minor | CinemaDNG / DNG decode (#1065) |
|
||||
| 2026-10-10 | major | Created: codec-by-codec comparison with Premiere 26.5.2 from the codec crates' READMEs, the export format registry and the installed bundle's listings |
|
||||
|
||||
+2
-1
@@ -165,7 +165,8 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||
substreams (played as the 5.1 core), AVI (#598; listed as an extension but no
|
||||
demuxer), WMV / ASF, DV / DVCPRO / DV100, MPEG-4 Part 2, FLAC in MP4 (#603), camera RAW (R3D
|
||||
incl. R3D NE, ARRIRAW, Sony RAW / X-OCN, Canon RAW, BRAW, ProRes RAW; #345, #383), JPEG 2000 /
|
||||
JPEG XS MXF, Cinema DNG; stills: PSD, EXR, HEIF / HEIC, Radiance HDR, AI / EPS, and the rarer
|
||||
JPEG XS MXF, the rest of CinemaDNG (lossy / JPEG XL DNG, MXF-wrapped, opcode lists, Raw
|
||||
settings; DNG frames and folders import since #1065); stills: PSD, EXR, HEIF / HEIC, Radiance HDR, AI / EPS, and the rarer
|
||||
DPX layouts (packed 10/12-bit, run-length, Y'CbCr; Targa and DPX stills and sequences import
|
||||
since #821, deeper than 8 bits kept at 8); transparent video (#402); Sony start timecode in
|
||||
`rtmd` (#460); stereo 3D flag (#500); MKV multi-audio (#601).
|
||||
|
||||
Reference in New Issue
Block a user