1use std::collections::BTreeSet;
4use std::sync::{Arc, LazyLock};
5
6use re_chunk::{Chunk, ChunkResult};
7use re_log_types::{ArrowMsg, EntityPath, LogMsg, RecordingId, StoreId, TimePoint};
8
9mod import_file;
12mod importer_archetype;
13mod importer_directory;
14mod importer_rrd;
15mod importer_urdf;
16
17#[cfg(not(target_arch = "wasm32"))]
18pub mod lerobot;
19
20#[cfg(not(target_arch = "wasm32"))]
22pub mod importer_lerobot;
23
24pub mod importer_mcap;
26
27#[cfg(not(target_arch = "wasm32"))]
28mod importer_external;
29#[cfg(not(target_arch = "wasm32"))]
30pub mod importer_parquet;
31
32pub use self::import_file::{import_from_file_contents, prepare_store_info};
33pub use self::importer_archetype::ArchetypeImporter;
34pub use self::importer_directory::DirectoryImporter;
35pub use self::importer_mcap::McapImporter;
36pub use self::importer_rrd::RrdImporter;
37pub use self::importer_urdf::{UrdfImporter, UrdfTree, joint_transform as urdf_joint_transform};
38#[cfg(not(target_arch = "wasm32"))]
39pub use self::{
40 import_file::import_from_path,
41 importer_external::{
42 EXTERNAL_IMPORTER_INCOMPATIBLE_EXIT_CODE, EXTERNAL_IMPORTER_PREFIX, ExternalImporter,
43 iter_external_importers,
44 },
45 importer_lerobot::LeRobotDatasetImporter,
46 importer_parquet::ParquetImporter,
47};
48
49pub mod external {
50 pub use urdf_rs;
51}
52
53pub const FOXGLOVE_LENSES_IDENTIFIER: &str = "foxglove";
57
58pub const URDF_DECODER_IDENTIFIER: &str = "urdf";
60
61pub fn supported_mcap_decoder_identifiers(
65 raw_fallback_enabled: bool,
66) -> Vec<re_mcap::DecoderIdentifier> {
67 let mut identifiers = re_mcap::DecoderRegistry::all_builtin(raw_fallback_enabled)
68 .all_identifiers()
69 .into_iter()
70 .map(re_mcap::DecoderIdentifier::from)
71 .collect::<BTreeSet<_>>();
72
73 identifiers.extend([
74 re_mcap::DecoderIdentifier::from(FOXGLOVE_LENSES_IDENTIFIER),
75 re_mcap::DecoderIdentifier::from(URDF_DECODER_IDENTIFIER),
76 ]);
77
78 identifiers.into_iter().collect()
79}
80
81#[derive(Debug, Clone)]
99pub struct ImporterSettings {
100 pub application_id: Option<re_log_types::ApplicationId>,
102
103 pub recording_id: RecordingId,
108
109 pub opened_store_id: Option<StoreId>,
111
112 pub force_store_info: bool,
117
118 pub entity_path_prefix: Option<EntityPath>,
120
121 pub timepoint: Option<TimePoint>,
123
124 pub follow: bool,
128
129 pub timestamp_offset_ns: Option<i64>,
131
132 pub timeline_type: re_log_types::TimeType,
138}
139
140impl ImporterSettings {
141 #[inline]
142 pub fn recommended(recording_id: impl Into<RecordingId>) -> Self {
143 Self {
144 recording_id: recording_id.into(),
145 application_id: None,
146 opened_store_id: None,
147 force_store_info: false,
148 entity_path_prefix: None,
149 timepoint: None,
150 follow: false,
151 timestamp_offset_ns: None,
152 timeline_type: re_log_types::TimeType::TimestampNs,
153 }
154 }
155
156 pub fn recommended_store_id(&self) -> StoreId {
158 StoreId::recording(
159 self.application_id
160 .clone()
161 .unwrap_or_else(re_log_types::ApplicationId::random),
162 self.recording_id.clone(),
163 )
164 }
165
166 pub fn opened_store_id_or_recommended(&self) -> StoreId {
169 self.opened_store_id
170 .clone()
171 .unwrap_or_else(|| self.recommended_store_id())
172 }
173
174 pub fn to_cli_args(&self) -> Vec<String> {
176 let Self {
177 application_id,
178 recording_id,
179 opened_store_id,
180 force_store_info: _,
181 entity_path_prefix,
182 timepoint,
183 follow: _,
184 timestamp_offset_ns: _,
185 timeline_type: _,
186 } = self;
187
188 let mut args = Vec::new();
189
190 if let Some(application_id) = application_id {
191 args.extend(["--application-id".to_owned(), format!("{application_id}")]);
192 }
193 args.extend(["--recording-id".to_owned(), format!("{recording_id}")]);
194
195 if let Some(opened_store_id) = opened_store_id {
196 args.extend([
197 "--opened-application-id".to_owned(),
198 format!("{}", opened_store_id.application_id()),
199 ]);
200
201 args.extend([
202 "--opened-recording-id".to_owned(),
203 format!("{}", opened_store_id.recording_id()),
204 ]);
205 }
206
207 if let Some(entity_path_prefix) = entity_path_prefix {
208 args.extend([
209 "--entity-path-prefix".to_owned(),
210 format!("{entity_path_prefix}"),
211 ]);
212 }
213
214 if let Some(timepoint) = timepoint {
215 if timepoint.is_static() {
216 args.push("--timeless".to_owned()); args.push("--static".to_owned());
218 }
219
220 for (timeline, cell) in timepoint.iter() {
221 match cell.typ() {
222 re_log_types::TimeType::Sequence => {
223 args.extend([
224 "--time_sequence".to_owned(),
225 format!("{timeline}={}", cell.value),
226 ]);
227
228 args.extend([
230 "--sequence".to_owned(),
231 format!("{timeline}={}", cell.value),
232 ]);
233 }
234 re_log_types::TimeType::DurationNs => {
235 args.extend([
236 "--time_duration_nanos".to_owned(),
237 format!("{timeline}={}", cell.value),
238 ]);
239
240 args.extend(["--time".to_owned(), format!("{timeline}={}", cell.value)]);
242 }
243 re_log_types::TimeType::TimestampNs => {
244 args.extend([
245 "--time_duration_nanos".to_owned(),
246 format!("{timeline}={}", cell.value),
247 ]);
248
249 args.extend([
251 "--sequence".to_owned(),
252 format!("{timeline}={}", cell.value),
253 ]);
254 }
255 }
256 }
257 }
258
259 args
260 }
261}
262
263pub type ImporterName = String;
264
265pub trait Importer: Send + Sync {
318 fn name(&self) -> ImporterName;
322
323 #[cfg(not(target_arch = "wasm32"))]
346 fn import_from_path(
347 &self,
348 settings: &ImporterSettings,
349 path: std::path::PathBuf,
350 tx: crossbeam::channel::Sender<ImportedData>,
351 ) -> Result<(), ImporterError>;
352
353 fn import_from_file_contents(
379 &self,
380 settings: &ImporterSettings,
381 filepath: std::path::PathBuf,
382 contents: std::borrow::Cow<'_, [u8]>,
383 tx: crossbeam::channel::Sender<ImportedData>,
384 ) -> Result<(), ImporterError>;
385}
386
387#[derive(thiserror::Error, Debug)]
389pub enum ImporterError {
390 #[cfg(not(target_arch = "wasm32"))]
391 #[error(transparent)]
392 IO(#[from] std::io::Error),
393
394 #[error(transparent)]
395 Arrow(#[from] arrow::error::ArrowError),
396
397 #[error(transparent)]
398 Chunk(#[from] re_chunk::ChunkError),
399
400 #[error(transparent)]
401 Decode(#[from] re_log_encoding::DecodeError),
402
403 #[error("No importer support for {0:?}")]
404 Incompatible(std::path::PathBuf),
405
406 #[error(transparent)]
407 Mcap(#[from] ::mcap::McapError),
408
409 #[error("Video file is too large ({} bytes). \
410 Maximum supported blob size is ~2 GiB due to Arrow i32 offset limits.", .0)]
411 VideoTooLarge(usize),
412
413 #[error("{}", re_error::format(.0))]
414 Other(#[from] anyhow::Error),
415}
416
417impl ImporterError {
418 #[inline]
419 pub fn is_path_not_found(&self) -> bool {
420 match self {
421 #[cfg(not(target_arch = "wasm32"))]
422 Self::IO(err) => err.kind() == std::io::ErrorKind::NotFound,
423 _ => false,
424 }
425 }
426
427 #[inline]
428 pub fn is_incompatible(&self) -> bool {
429 matches!(self, Self::Incompatible { .. })
430 }
431}
432
433#[derive(Debug)]
439pub enum ImportedData {
440 Chunk(ImporterName, re_log_types::StoreId, Chunk),
441 ArrowMsg(ImporterName, re_log_types::StoreId, ArrowMsg),
442 LogMsg(ImporterName, LogMsg),
443}
444
445impl ImportedData {
446 #[inline]
448 pub fn importer_name(&self) -> &ImporterName {
449 match self {
450 Self::Chunk(name, ..) | Self::ArrowMsg(name, ..) | Self::LogMsg(name, ..) => name,
451 }
452 }
453
454 #[inline]
456 pub fn into_log_msg(self) -> ChunkResult<LogMsg> {
457 match self {
458 Self::Chunk(_name, store_id, chunk) => {
459 Ok(LogMsg::ArrowMsg(store_id, chunk.to_arrow_msg()?))
460 }
461
462 Self::ArrowMsg(_name, store_id, msg) => Ok(LogMsg::ArrowMsg(store_id, msg)),
463
464 Self::LogMsg(_name, msg) => Ok(msg),
465 }
466 }
467
468 pub fn into_chunk(self) -> Option<Chunk> {
470 match self {
471 Self::Chunk(_name, _store_id, chunk) => Some(chunk),
472 Self::ArrowMsg(_name, _store_id, arrow_msg) => Chunk::from_arrow_msg(&arrow_msg).ok(),
473 Self::LogMsg(_name, msg) => match msg {
474 LogMsg::ArrowMsg(_store_id, arrow_msg) => Chunk::from_arrow_msg(&arrow_msg).ok(),
475 LogMsg::SetStoreInfo { .. } | LogMsg::BlueprintActivationCommand { .. } => None,
476 },
477 }
478 }
479}
480
481static BUILTIN_IMPORTERS: LazyLock<Vec<Arc<dyn Importer>>> = LazyLock::new(|| {
487 vec![
488 Arc::new(RrdImporter) as Arc<dyn Importer>,
489 Arc::new(ArchetypeImporter),
490 Arc::new(DirectoryImporter),
491 Arc::new(McapImporter::default()),
492 #[cfg(not(target_arch = "wasm32"))]
493 Arc::new(ParquetImporter::default()),
494 #[cfg(not(target_arch = "wasm32"))]
495 Arc::new(LeRobotDatasetImporter),
496 #[cfg(not(target_arch = "wasm32"))]
497 Arc::new(ExternalImporter),
498 Arc::new(UrdfImporter),
499 ]
500});
501
502#[inline]
504pub fn iter_importers() -> impl Iterator<Item = Arc<dyn Importer>> {
505 BUILTIN_IMPORTERS
506 .clone()
507 .into_iter()
508 .chain(CUSTOM_IMPORTERS.read().clone())
509}
510
511static CUSTOM_IMPORTERS: LazyLock<parking_lot::RwLock<Vec<Arc<dyn Importer>>>> =
515 LazyLock::new(parking_lot::RwLock::default);
516
517#[inline]
522pub fn register_custom_importer(importer: impl Importer + 'static) {
523 CUSTOM_IMPORTERS.write().push(Arc::new(importer));
524}
525
526#[inline]
530pub(crate) fn extension(path: &std::path::Path) -> String {
531 path.extension()
532 .unwrap_or_default()
533 .to_ascii_lowercase()
534 .to_string_lossy()
535 .to_string()
536}
537
538pub const SUPPORTED_IMAGE_EXTENSIONS: &[&str] = &[
542 "avif", "bmp", "dds", "exr", "farbfeld", "ff", "gif", "hdr", "ico", "jpeg", "jpg", "pam",
543 "pbm", "pgm", "png", "ppm", "tga", "tif", "tiff", "webp",
544];
545
546pub const SUPPORTED_DEPTH_IMAGE_EXTENSIONS: &[&str] = &["rvl", "png"];
547
548pub const SUPPORTED_VIDEO_EXTENSIONS: &[&str] = &["mp4"];
549
550pub const SUPPORTED_MESH_EXTENSIONS: &[&str] = &["glb", "gltf", "obj", "stl", "dae"];
551
552pub const SUPPORTED_POINT_CLOUD_EXTENSIONS: &[&str] = &["ply"];
554
555pub const SUPPORTED_RERUN_EXTENSIONS: &[&str] = &["rbl", "rrd"];
556
557pub const SUPPORTED_THIRD_PARTY_FORMATS: &[&str] = &["mcap", "urdf"];
559
560pub const SUPPORTED_PARQUET_EXTENSIONS: &[&str] = &["parquet"];
561
562pub const SUPPORTED_TEXT_EXTENSIONS: &[&str] = &["txt", "md"];
564
565pub fn supported_extensions() -> impl Iterator<Item = &'static str> {
567 SUPPORTED_RERUN_EXTENSIONS
568 .iter()
569 .chain(SUPPORTED_THIRD_PARTY_FORMATS)
570 .chain(SUPPORTED_IMAGE_EXTENSIONS)
571 .chain(SUPPORTED_DEPTH_IMAGE_EXTENSIONS)
572 .chain(SUPPORTED_VIDEO_EXTENSIONS)
573 .chain(SUPPORTED_MESH_EXTENSIONS)
574 .chain(SUPPORTED_POINT_CLOUD_EXTENSIONS)
575 .chain(SUPPORTED_PARQUET_EXTENSIONS)
576 .chain(SUPPORTED_TEXT_EXTENSIONS)
577 .copied()
578}
579
580pub fn is_supported_file_extension(extension: &str) -> bool {
582 re_log::debug_assert!(
583 !extension.starts_with('.'),
584 "Expected extension without period, but got {extension:?}"
585 );
586 let extension = extension.to_lowercase();
587 supported_extensions().any(|ext| ext == extension)
588}
589
590pub fn detect_format_from_bytes(bytes: &[u8]) -> Option<String> {
597 let media_type = re_sdk_types::components::MediaType::guess_from_data(bytes)?;
598 media_type.file_extension().map(|e| e.to_owned())
599}
600
601pub fn content_type_to_extension(content_type: &str) -> Option<String> {
608 let mime = content_type.split(';').next()?.trim();
610
611 if mime == "application/octet-stream" {
613 return None;
614 }
615
616 let media_type = re_sdk_types::components::MediaType(mime.to_owned().into());
617 media_type.file_extension().map(|e| e.to_owned())
618}
619
620#[test]
621fn test_supported_extensions() {
622 assert!(is_supported_file_extension("rrd"));
623 assert!(is_supported_file_extension("mcap"));
624 assert!(is_supported_file_extension("png"));
625 assert!(is_supported_file_extension("urdf"));
626}
627
628#[test]
629fn test_supported_mcap_decoder_identifiers() {
630 let identifiers = supported_mcap_decoder_identifiers(true);
631 let as_strings = identifiers
632 .iter()
633 .map(ToString::to_string)
634 .collect::<Vec<_>>();
635
636 assert!(as_strings.contains(&FOXGLOVE_LENSES_IDENTIFIER.to_owned()));
638 assert!(as_strings.contains(&URDF_DECODER_IDENTIFIER.to_owned()));
639 assert!(as_strings.contains(&"attachments".to_owned()));
640 assert!(as_strings.contains(&"raw".to_owned()));
641 assert!(as_strings.contains(&"protobuf".to_owned()));
642 assert!(as_strings.contains(&"ros2msg".to_owned()));
643
644 let unique = as_strings.iter().collect::<std::collections::BTreeSet<_>>();
646 assert_eq!(as_strings.len(), unique.len());
647}
648
649#[test]
650fn test_detect_format_from_bytes() {
651 assert_eq!(
652 detect_format_from_bytes(b"RRF2xxxxx").as_deref(),
653 Some("rrd")
654 );
655 assert_eq!(
656 detect_format_from_bytes(b"RRF0xxxxx").as_deref(),
657 Some("rrd")
658 );
659 assert_eq!(
660 detect_format_from_bytes(&[0x89, 0x4D, 0x43, 0x41, 0x50, 0x30, 0x0D, 0x0A]).as_deref(),
661 Some("mcap")
662 );
663 assert_eq!(
664 detect_format_from_bytes(&[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]).as_deref(),
665 Some("png")
666 );
667 assert_eq!(
668 detect_format_from_bytes(&[0xFF, 0xD8, 0xFF, 0xE0]).as_deref(),
669 Some("jpg")
670 );
671 assert_eq!(
672 detect_format_from_bytes(b"glTFxxxx").as_deref(),
673 Some("glb")
674 );
675 assert_eq!(
676 detect_format_from_bytes(b"ply\nxxx").as_deref(),
677 Some("ply")
678 );
679 assert_eq!(detect_format_from_bytes(b"unknown").as_deref(), None);
680 assert_eq!(detect_format_from_bytes(b"").as_deref(), None);
681}
682
683#[test]
684fn test_content_type_to_extension() {
685 assert_eq!(
686 content_type_to_extension("image/png").as_deref(),
687 Some("png")
688 );
689 assert_eq!(
690 content_type_to_extension("image/png; charset=utf-8").as_deref(),
691 Some("png")
692 );
693 assert_eq!(
694 content_type_to_extension("image/jpeg").as_deref(),
695 Some("jpg")
696 );
697 assert_eq!(
698 content_type_to_extension("video/mp4").as_deref(),
699 Some("mp4")
700 );
701 assert_eq!(
702 content_type_to_extension("model/gltf-binary").as_deref(),
703 Some("glb")
704 );
705 assert_eq!(
706 content_type_to_extension("application/x-rerun").as_deref(),
707 Some("rrd")
708 );
709 assert_eq!(
710 content_type_to_extension("application/octet-stream").as_deref(),
711 None
712 );
713}