Skip to content

Commit

Permalink
feat: added support for RAF file type
Browse files Browse the repository at this point in the history
  • Loading branch information
mindeng committed Oct 23, 2024
1 parent 35c9b66 commit e56c685
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 20 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Cargo.lock
.DS_Store
/testdata/*.html
/debug.log
/testdata/fujifilm_x_t1_01.raf
13 changes: 0 additions & 13 deletions src/bbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,19 +321,6 @@ impl<O, T: ParseBody<O>> ParseBox<O> for T {
}
}

fn parse_cstr(input: &[u8]) -> IResult<&[u8], String> {
let (remain, s) = map_res(streaming::take_till(|b| b == 0), |bs: &[u8]| {
if bs.is_empty() {
Ok("".to_owned())
} else {
String::from_utf8(bs.to_vec())
}
})(input)?;

// consumes the zero byte
Ok((&remain[1..], s)) // Safe-slice
}

#[cfg(test)]
mod tests {
use crate::testkit::read_sample;
Expand Down
4 changes: 2 additions & 2 deletions src/bbox/iinf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ use nom::{
IResult,
};

use crate::bbox::FullBoxHeader;
use crate::{bbox::FullBoxHeader, utils::parse_cstr};

use super::{parse_cstr, ParseBody, ParseBox};
use super::{ParseBody, ParseBox};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IinfBox {
Expand Down
12 changes: 7 additions & 5 deletions src/exif.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::error::{nom_error_to_parsing_error_with_state, ParsingError, ParsingErrorState};
use crate::file::MimeImage;
use crate::parser::{BufParser, ParsingState, ShareBuf};
use crate::raf::RafInfo;
use crate::skip::Skip;
use crate::slice::SubsliceRange;
use crate::{heif, jpeg, MediaParser, MediaSource};
Expand Down Expand Up @@ -134,13 +135,11 @@ pub(crate) fn extract_exif_with_mime(
state: Option<ParsingState>,
) -> Result<(Option<&[u8]>, Option<ParsingState>), ParsingErrorState> {
let (exif_data, state) = match img_type {
crate::file::MimeImage::Jpeg => jpeg::extract_exif_data(buf)
MimeImage::Jpeg => jpeg::extract_exif_data(buf)
.map(|res| (res.1, state.clone()))
.map_err(|e| nom_error_to_parsing_error_with_state(e, state))?,
crate::file::MimeImage::Heic | crate::file::MimeImage::Heif => {
heif_extract_exif(state, buf)?
}
crate::file::MimeImage::Tiff => {
MimeImage::Heic | crate::file::MimeImage::Heif => heif_extract_exif(state, buf)?,
MimeImage::Tiff => {
let (header, data_start) = match state {
Some(ParsingState::TiffHeader(ref h)) => (h.to_owned(), 0),
None => {
Expand All @@ -166,6 +165,9 @@ pub(crate) fn extract_exif_with_mime(

(Some(buf), state)
}
MimeImage::Raf => RafInfo::parse(buf)
.map(|res| (res.1.exif_data, state.clone()))
.map_err(|e| nom_error_to_parsing_error_with_state(e, state))?,
};
Ok((exif_data, state))
}
Expand Down
1 change: 1 addition & 0 deletions src/exif/exif_iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ mod tests {
#[test_case("broken.jpg", "", MimeImage::Jpeg)]
#[test_case("exif.heic", "+08:00", MimeImage::Heic)]
#[test_case("tif.tif", "", MimeImage::Tiff)]
#[test_case("fujifilm_x_t1_01.raf", "", MimeImage::Raf)]
fn exif_iter_tz(path: &str, tz: &str, img_type: MimeImage) {
let buf = read_sample(path).unwrap();
let (data, _) = extract_exif_with_mime(img_type, &buf, None).unwrap();
Expand Down
5 changes: 5 additions & 0 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
exif::TiffHeader,
jpeg::check_jpeg,
loader::Load,
raf::RafInfo,
slice::SubsliceRange,
};

Expand Down Expand Up @@ -63,6 +64,7 @@ pub(crate) enum MimeImage {
Heic,
Heif,
Tiff,
Raf, // Fujifilm RAW, image/x-fuji-raf
}

#[derive(Debug, Clone, PartialEq, Eq, Copy)]
Expand All @@ -89,6 +91,8 @@ impl TryFrom<&[u8]> for Mime {
Mime::Image(MimeImage::Tiff)
} else if check_jpeg(input).is_ok() {
Mime::Image(MimeImage::Jpeg)
} else if RafInfo::check(input).is_ok() {
Mime::Image(MimeImage::Raf)
} else {
return Err(crate::Error::UnrecognizedFileFormat);
};
Expand Down Expand Up @@ -363,6 +367,7 @@ mod tests {

#[test_case("exif.heic", Image(Heic))]
#[test_case("exif.jpg", Image(Jpeg))]
#[test_case("fujifilm_x_t1_01.raf", Image(Raf))]
#[test_case("meta.mp4", Video(Mp4))]
#[test_case("meta.mov", Video(QuickTime))]
#[test_case("embedded-in-heic.mov", Video(QuickTime))]
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,10 @@ mod parser;
#[cfg(feature = "async")]
mod parser_async;
mod partial_vec;
mod raf;
mod skip;
mod slice;
mod utils;
mod values;
mod video;

Expand Down
1 change: 1 addition & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ mod tests {
#[case("embedded-in-heic.mov", Track)]
#[case("exif.heic", Exif)]
#[case("exif.jpg", Exif)]
#[case("fujifilm_x_t1_01.raf", Exif)]
#[case("meta.mov", Track)]
#[case("meta.mp4", Track)]
#[case("mka.mka", Track)]
Expand Down
112 changes: 112 additions & 0 deletions src/raf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use nom::{
bytes::streaming::{tag, take},
number, IResult,
};

use crate::{jpeg, utils::parse_cstr};

const MAGIC: &[u8] = b"FUJIFILMCCD-RAW ";

/// Refer to: [Fujifilm RAF](http://fileformats.archiveteam.org/wiki/Fujifilm_RAF)
#[allow(unused)]
pub struct RafInfo<'a> {
pub version: &'a [u8],
pub camera_num_id: &'a [u8],
pub camera_string: String,
pub directory_ver: &'a [u8],
pub image_offset: u32,
pub exif_data: Option<&'a [u8]>,
}

impl RafInfo<'_> {
pub fn check(input: &[u8]) -> crate::Result<()> {
// check magic
let _ = nom::bytes::complete::tag(MAGIC)(input)?;
Ok(())
}

pub(crate) fn parse(input: &[u8]) -> IResult<&[u8], RafInfo> {
// magic
let (remain, _) = tag(MAGIC)(input)?;
let (remain, version) = take(4usize)(remain)?;
let (remain, camera_num_id) = take(8usize)(remain)?;
let (remain, camera_string) = take(32usize)(remain)?;
let (remain, directory_ver) = take(4usize)(remain)?;

// 20 bytes unknown
let (remain, _) = take(20usize)(remain)?;

let (remain, image_offset) = number::streaming::be_u32(remain)?;

// skip to image_offset
let skip_n = image_offset
.checked_sub((input.len() - remain.len()) as u32)
.ok_or_else(|| {
nom::Err::Failure(nom::error::make_error(remain, nom::error::ErrorKind::Fail))
})?;
let (remain, _) = take(skip_n)(remain)?;

// parse as a JPEG
jpeg::check_jpeg(remain).map_err(|_| {
nom::Err::Failure(nom::error::make_error(remain, nom::error::ErrorKind::Fail))
})?;
let (remain, exif_data) = jpeg::extract_exif_data(remain)?;

let (_, camera_string) = parse_cstr(camera_string)?;

Ok((
remain,
RafInfo {
version,
camera_num_id,
camera_string,
directory_ver,
image_offset,
exif_data,
},
))
}
}

#[cfg(test)]
mod tests {
use std::{fs::File, io::Write, path::Path};

use test_case::case;

use crate::testkit::read_sample;

use super::*;

#[case("fujifilm_x_t1_01.raf")]
fn test_check_raf(path: &str) {
let data = read_sample(path).unwrap();
RafInfo::check(&data).unwrap();
}

// #[case("fujifilm_x_t1_01.raf", b"0201", b"FF119503", "X-T1", 0x94)]
#[case("fujifilm_x_t1_01.raf.meta", b"0201", b"FF119503", "X-T1", 0x94)]
fn test_extract_exif(
path: &str,
version: &[u8],
camera_num_id: &[u8],
camera_string: &str,
image_offset: u32,
) {
let data = read_sample(path).unwrap();
let (remain, raf) = RafInfo::parse(&data).unwrap();
assert_eq!(raf.version, version);
assert_eq!(raf.camera_num_id, camera_num_id);
assert_eq!(raf.camera_string, camera_string);
assert_eq!(raf.image_offset, image_offset);
raf.exif_data.unwrap();

// save header + exif_data
let p = Path::new("./testdata").join("fujifilm_x_t1_01.raf.meta");
if !p.exists() {
let size = data.len() - remain.len();
let mut f = File::create(p).unwrap();
f.write_all(&data[..size]).unwrap();
}
}
}
38 changes: 38 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use nom::{combinator::map_res, IResult};

pub(crate) fn parse_cstr(input: &[u8]) -> IResult<&[u8], String> {
let (remain, s) = map_res(
nom::bytes::streaming::take_till(|b| b == 0),
|bs: &[u8]| {
if bs.is_empty() {
Ok("".to_owned())
} else {
String::from_utf8(bs.to_vec())
}
},
)(input)?;

// consumes the zero byte
Ok((&remain[1..], s)) // Safe-slice
}

#[cfg(test)]
mod tests {
use super::*;
use test_case::case;

#[case(b"", None)]
#[case(b"\0", Some(""))]
#[case(b"h\0", Some("h"))]
#[case(b"hello\0", Some("hello"))]
#[case(b"hello", None)]
fn test_check_raf(data: &[u8], expect: Option<&str>) {
let res = parse_cstr(data);
match expect {
Some(s) => assert_eq!(res.unwrap().1, s),
None => {
res.unwrap_err();
}
}
}
}
Binary file added testdata/fujifilm_x_t1_01.raf.meta
Binary file not shown.

0 comments on commit e56c685

Please sign in to comment.