mp4forge 0.8.0

Rust library and CLI for inspecting, probing, extracting, muxing, and rewriting MP4 structures
Documentation
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

#[cfg(feature = "async")]
use tokio::fs::File as TokioFile;
#[cfg(feature = "async")]
use tokio::io::{AsyncReadExt, AsyncSeekExt};

use super::super::MuxError;
use super::super::import::{
    SegmentedMuxSourceSegment, SegmentedMuxSourceSegmentData, SegmentedMuxSourceSpec,
};
use super::raw_visual::{UncvPixelLayout, build_uncv_sample_entry_box};

pub(in crate::mux) struct ParsedBmpTrack {
    pub(in crate::mux) width: u16,
    pub(in crate::mux) height: u16,
    pub(in crate::mux) sample_entry_box: Vec<u8>,
    pub(in crate::mux) segmented_source: SegmentedMuxSourceSpec,
}

pub(in crate::mux) fn scan_bmp_file_sync(
    path: &Path,
    spec: &str,
) -> Result<ParsedBmpTrack, MuxError> {
    let mut file = File::open(path)?;
    let file_size = file.metadata()?.len();
    parse_bmp_file_sync(path, spec, &mut file, file_size)
}

#[cfg(feature = "async")]
pub(in crate::mux) async fn scan_bmp_file_async(
    path: &Path,
    spec: &str,
) -> Result<ParsedBmpTrack, MuxError> {
    let mut file = TokioFile::open(path).await?;
    let file_size = file.metadata().await?.len();
    parse_bmp_file_async(path, spec, &mut file, file_size).await
}

fn parse_bmp_file_sync(
    path: &Path,
    spec: &str,
    file: &mut File,
    file_size: u64,
) -> Result<ParsedBmpTrack, MuxError> {
    if file_size < 54 {
        return Err(invalid_bmp(
            spec,
            "BMP input is truncated before the 54-byte file and info headers",
        ));
    }
    let mut header = [0_u8; 54];
    file.seek(SeekFrom::Start(0))?;
    file.read_exact(&mut header)?;
    if &header[..2] != b"BM" {
        return Err(invalid_bmp(
            spec,
            "input does not start with the BMP file signature",
        ));
    }

    let data_offset = usize::try_from(read_le_u32(&header, 10, spec, "pixel data offset")?)
        .map_err(|_| MuxError::LayoutOverflow("BMP data offset"))?;
    let dib_header_size = read_le_u32(&header, 14, spec, "DIB header size")?;
    if dib_header_size < 40 {
        return Err(invalid_bmp(
            spec,
            "BMP DIB header is smaller than the required 40-byte BITMAPINFOHEADER layout",
        ));
    }

    let width_signed = read_le_i32(&header, 18, spec, "width")?;
    let height_signed = read_le_i32(&header, 22, spec, "height")?;
    if width_signed <= 0 || height_signed == 0 {
        return Err(invalid_bmp(
            spec,
            "BMP header declared a non-positive width or a zero height",
        ));
    }
    let top_down = height_signed < 0;
    let width_abs = u32::try_from(width_signed)
        .map_err(|_| invalid_bmp(spec, "BMP width does not fit in a positive 32-bit size"))?;
    let height_abs = height_signed.unsigned_abs();
    let width = u16::try_from(width_abs)
        .map_err(|_| invalid_bmp(spec, "BMP width does not fit in an MP4 visual sample entry"))?;
    let height = u16::try_from(height_abs).map_err(|_| {
        invalid_bmp(
            spec,
            "BMP height does not fit in an MP4 visual sample entry",
        )
    })?;

    let planes = read_le_u16(&header, 26, spec, "plane count")?;
    if planes != 1 {
        return Err(invalid_bmp(
            spec,
            "BMP input declared a plane count other than one",
        ));
    }
    let bits_per_pixel = read_le_u16(&header, 28, spec, "bits per pixel")?;
    let compression = read_le_u32(&header, 30, spec, "compression")?;
    if compression != 0 {
        return Err(invalid_bmp(
            spec,
            "BMP input used a compressed pixel layout; only BI_RGB carriage is supported",
        ));
    }

    let (layout, bytes_per_pixel) = match bits_per_pixel {
        24 => (UncvPixelLayout::Rgb24, 3_u64),
        32 => (UncvPixelLayout::Rgbx32, 4_u64),
        _ => {
            return Err(invalid_bmp(
                spec,
                "BMP input declared an unsupported bit depth; only 24-bit and 32-bit BI_RGB carriage is supported",
            ));
        }
    };

    let row_stride = (u64::from(width_abs) * u64::from(bits_per_pixel)).div_ceil(32) * 4;
    let data_end = u64::try_from(data_offset)
        .unwrap()
        .checked_add(
            row_stride
                .checked_mul(u64::from(height_abs))
                .ok_or(MuxError::LayoutOverflow("BMP pixel payload size"))?,
        )
        .ok_or(MuxError::LayoutOverflow("BMP pixel payload range"))?;
    if data_end > file_size {
        return Err(invalid_bmp(
            spec,
            "BMP pixel payload overruns the input length",
        ));
    }

    let transformed_len = u64::from(width_abs)
        .checked_mul(u64::from(height_abs))
        .and_then(|value| value.checked_mul(bytes_per_pixel))
        .ok_or(MuxError::LayoutOverflow("BMP transformed payload size"))?;
    let transformed_capacity = usize::try_from(transformed_len)
        .map_err(|_| MuxError::LayoutOverflow("BMP transformed payload size"))?;
    let mut transformed = Vec::with_capacity(transformed_capacity);
    let row_stride_usize =
        usize::try_from(row_stride).map_err(|_| MuxError::LayoutOverflow("BMP row stride"))?;
    let mut row = vec![0_u8; row_stride_usize];
    for output_row in 0..height_abs {
        let source_row = if top_down {
            output_row
        } else {
            height_abs - 1 - output_row
        };
        let row_start = u64::try_from(data_offset)
            .unwrap()
            .checked_add(u64::from(source_row) * row_stride)
            .ok_or(MuxError::LayoutOverflow("BMP row offset"))?;
        file.seek(SeekFrom::Start(row_start))?;
        file.read_exact(&mut row)?;
        for column in 0..width_abs {
            let pixel_offset = usize::try_from(u64::from(column) * bytes_per_pixel)
                .map_err(|_| MuxError::LayoutOverflow("BMP pixel offset"))?;
            match bits_per_pixel {
                24 => {
                    transformed.push(row[pixel_offset + 2]);
                    transformed.push(row[pixel_offset + 1]);
                    transformed.push(row[pixel_offset]);
                }
                32 => {
                    transformed.push(row[pixel_offset + 2]);
                    transformed.push(row[pixel_offset + 1]);
                    transformed.push(row[pixel_offset]);
                    transformed.push(row[pixel_offset + 3]);
                }
                _ => unreachable!(),
            }
        }
    }

    let sample_entry_box = build_uncv_sample_entry_box(width, height, layout, false, false)?;
    let total_size = u64::try_from(transformed.len())
        .map_err(|_| MuxError::LayoutOverflow("BMP transformed payload size"))?;
    Ok(ParsedBmpTrack {
        width,
        height,
        sample_entry_box,
        segmented_source: SegmentedMuxSourceSpec {
            path: path.to_path_buf(),
            segments: vec![SegmentedMuxSourceSegment {
                logical_offset: 0,
                data: SegmentedMuxSourceSegmentData::Bytes(transformed),
            }],
            total_size,
        },
    })
}

#[cfg(feature = "async")]
async fn parse_bmp_file_async(
    path: &Path,
    spec: &str,
    file: &mut TokioFile,
    file_size: u64,
) -> Result<ParsedBmpTrack, MuxError> {
    if file_size < 54 {
        return Err(invalid_bmp(
            spec,
            "BMP input is truncated before the 54-byte file and info headers",
        ));
    }
    let mut header = [0_u8; 54];
    file.seek(SeekFrom::Start(0)).await?;
    file.read_exact(&mut header).await?;
    if &header[..2] != b"BM" {
        return Err(invalid_bmp(
            spec,
            "input does not start with the BMP file signature",
        ));
    }

    let data_offset = usize::try_from(read_le_u32(&header, 10, spec, "pixel data offset")?)
        .map_err(|_| MuxError::LayoutOverflow("BMP data offset"))?;
    let dib_header_size = read_le_u32(&header, 14, spec, "DIB header size")?;
    if dib_header_size < 40 {
        return Err(invalid_bmp(
            spec,
            "BMP DIB header is smaller than the required 40-byte BITMAPINFOHEADER layout",
        ));
    }

    let width_signed = read_le_i32(&header, 18, spec, "width")?;
    let height_signed = read_le_i32(&header, 22, spec, "height")?;
    if width_signed <= 0 || height_signed == 0 {
        return Err(invalid_bmp(
            spec,
            "BMP header declared a non-positive width or a zero height",
        ));
    }
    let top_down = height_signed < 0;
    let width_abs = u32::try_from(width_signed)
        .map_err(|_| invalid_bmp(spec, "BMP width does not fit in a positive 32-bit size"))?;
    let height_abs = height_signed.unsigned_abs();
    let width = u16::try_from(width_abs)
        .map_err(|_| invalid_bmp(spec, "BMP width does not fit in an MP4 visual sample entry"))?;
    let height = u16::try_from(height_abs).map_err(|_| {
        invalid_bmp(
            spec,
            "BMP height does not fit in an MP4 visual sample entry",
        )
    })?;

    let planes = read_le_u16(&header, 26, spec, "plane count")?;
    if planes != 1 {
        return Err(invalid_bmp(
            spec,
            "BMP input declared a plane count other than one",
        ));
    }
    let bits_per_pixel = read_le_u16(&header, 28, spec, "bits per pixel")?;
    let compression = read_le_u32(&header, 30, spec, "compression")?;
    if compression != 0 {
        return Err(invalid_bmp(
            spec,
            "BMP input used a compressed pixel layout; only BI_RGB carriage is supported",
        ));
    }

    let (layout, bytes_per_pixel) = match bits_per_pixel {
        24 => (UncvPixelLayout::Rgb24, 3_u64),
        32 => (UncvPixelLayout::Rgbx32, 4_u64),
        _ => {
            return Err(invalid_bmp(
                spec,
                "BMP input declared an unsupported bit depth; only 24-bit and 32-bit BI_RGB carriage is supported",
            ));
        }
    };

    let row_stride = (u64::from(width_abs) * u64::from(bits_per_pixel)).div_ceil(32) * 4;
    let data_end = u64::try_from(data_offset)
        .unwrap()
        .checked_add(
            row_stride
                .checked_mul(u64::from(height_abs))
                .ok_or(MuxError::LayoutOverflow("BMP pixel payload size"))?,
        )
        .ok_or(MuxError::LayoutOverflow("BMP pixel payload range"))?;
    if data_end > file_size {
        return Err(invalid_bmp(
            spec,
            "BMP pixel payload overruns the input length",
        ));
    }

    let transformed_len = u64::from(width_abs)
        .checked_mul(u64::from(height_abs))
        .and_then(|value| value.checked_mul(bytes_per_pixel))
        .ok_or(MuxError::LayoutOverflow("BMP transformed payload size"))?;
    let transformed_capacity = usize::try_from(transformed_len)
        .map_err(|_| MuxError::LayoutOverflow("BMP transformed payload size"))?;
    let mut transformed = Vec::with_capacity(transformed_capacity);
    let row_stride_usize =
        usize::try_from(row_stride).map_err(|_| MuxError::LayoutOverflow("BMP row stride"))?;
    let mut row = vec![0_u8; row_stride_usize];
    for output_row in 0..height_abs {
        let source_row = if top_down {
            output_row
        } else {
            height_abs - 1 - output_row
        };
        let row_start = u64::try_from(data_offset)
            .unwrap()
            .checked_add(u64::from(source_row) * row_stride)
            .ok_or(MuxError::LayoutOverflow("BMP row offset"))?;
        file.seek(SeekFrom::Start(row_start)).await?;
        file.read_exact(&mut row).await?;
        for column in 0..width_abs {
            let pixel_offset = usize::try_from(u64::from(column) * bytes_per_pixel)
                .map_err(|_| MuxError::LayoutOverflow("BMP pixel offset"))?;
            match bits_per_pixel {
                24 => {
                    transformed.push(row[pixel_offset + 2]);
                    transformed.push(row[pixel_offset + 1]);
                    transformed.push(row[pixel_offset]);
                }
                32 => {
                    transformed.push(row[pixel_offset + 2]);
                    transformed.push(row[pixel_offset + 1]);
                    transformed.push(row[pixel_offset]);
                    transformed.push(row[pixel_offset + 3]);
                }
                _ => unreachable!(),
            }
        }
    }

    let sample_entry_box = build_uncv_sample_entry_box(width, height, layout, false, false)?;
    let total_size = u64::try_from(transformed.len())
        .map_err(|_| MuxError::LayoutOverflow("BMP transformed payload size"))?;
    Ok(ParsedBmpTrack {
        width,
        height,
        sample_entry_box,
        segmented_source: SegmentedMuxSourceSpec {
            path: path.to_path_buf(),
            segments: vec![SegmentedMuxSourceSegment {
                logical_offset: 0,
                data: SegmentedMuxSourceSegmentData::Bytes(transformed),
            }],
            total_size,
        },
    })
}

fn read_le_u16(bytes: &[u8], offset: usize, spec: &str, field: &str) -> Result<u16, MuxError> {
    let slice = bytes
        .get(offset..offset + 2)
        .ok_or_else(|| invalid_bmp(spec, &format!("BMP header is truncated before the {field}")))?;
    Ok(u16::from_le_bytes(slice.try_into().unwrap()))
}

fn read_le_u32(bytes: &[u8], offset: usize, spec: &str, field: &str) -> Result<u32, MuxError> {
    let slice = bytes
        .get(offset..offset + 4)
        .ok_or_else(|| invalid_bmp(spec, &format!("BMP header is truncated before the {field}")))?;
    Ok(u32::from_le_bytes(slice.try_into().unwrap()))
}

fn read_le_i32(bytes: &[u8], offset: usize, spec: &str, field: &str) -> Result<i32, MuxError> {
    let slice = bytes
        .get(offset..offset + 4)
        .ok_or_else(|| invalid_bmp(spec, &format!("BMP header is truncated before the {field}")))?;
    Ok(i32::from_le_bytes(slice.try_into().unwrap()))
}

fn invalid_bmp(spec: &str, message: &str) -> MuxError {
    MuxError::UnsupportedTrackImport {
        spec: spec.to_string(),
        message: message.to_string(),
    }
}