Use json for ffmpeg, imagemagick details parsing

This commit is contained in:
asonix 2023-07-09 14:50:58 -05:00
parent fa74e3c82d
commit 1694f49436
23 changed files with 11742 additions and 99 deletions

View file

@ -66,6 +66,9 @@ pub(crate) enum UploadError {
#[error("Error in store")]
Store(#[source] crate::store::StoreError),
#[error("Error parsing image details")]
ParseDetails(#[from] crate::magick::ParseDetailsError),
#[error("Provided process path is invalid")]
ParsePath,

View file

@ -1,3 +1,6 @@
#[cfg(test)]
mod tests;
use crate::{
config::{AudioCodec, ImageFormat, MediaConfiguration, VideoCodec},
error::{Error, UploadError},
@ -368,6 +371,22 @@ pub(crate) async fn details_bytes(input: Bytes) -> Result<Option<Details>, Error
.await
}
#[derive(serde::Deserialize)]
struct PixelFormatOutput {
pixel_formats: Vec<PixelFormat>,
}
#[derive(serde::Deserialize)]
struct PixelFormat {
name: String,
flags: Flags,
}
#[derive(serde::Deserialize)]
struct Flags {
alpha: usize,
}
async fn alpha_pixel_formats() -> Result<HashSet<String>, Error> {
let process = Process::run(
"ffprobe",
@ -378,33 +397,49 @@ async fn alpha_pixel_formats() -> Result<HashSet<String>, Error> {
"pixel_format=name:flags=alpha",
"-of",
"compact=p=0",
"-print_format",
"json",
],
)?;
let mut output = Vec::new();
process.read().read_to_end(&mut output).await?;
let output = String::from_utf8_lossy(&output);
let formats = output
.split('\n')
.filter_map(|format| {
if format.is_empty() {
let formats: PixelFormatOutput = serde_json::from_slice(&output)?;
Ok(parse_pixel_formats(formats))
}
fn parse_pixel_formats(formats: PixelFormatOutput) -> HashSet<String> {
formats
.pixel_formats
.into_iter()
.filter_map(|PixelFormat { name, flags }| {
if flags.alpha == 0 {
return None;
}
if !format.ends_with('1') {
return None;
}
Some(
format
.trim_start_matches("name=")
.trim_end_matches("|flags:alpha=1")
.to_string(),
)
Some(name)
})
.collect();
.collect()
}
Ok(formats)
#[derive(Debug, serde::Deserialize)]
struct DetailsOutput {
streams: [Stream; 1],
format: Format,
}
#[derive(Debug, serde::Deserialize)]
struct Stream {
width: usize,
height: usize,
nb_read_frames: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
struct Format {
format_name: String,
}
#[tracing::instrument(skip(f))]
@ -435,46 +470,35 @@ where
"stream=width,height,nb_read_frames:format=format_name",
"-of",
"default=noprint_wrappers=1:nokey=1",
"-print_format",
"json",
input_file_str,
],
)?;
let mut output = Vec::new();
process.read().read_to_end(&mut output).await?;
let output = String::from_utf8_lossy(&output);
tokio::fs::remove_file(input_file_str).await?;
let output: DetailsOutput = serde_json::from_slice(&output)?;
parse_details(output)
}
fn parse_details(output: std::borrow::Cow<'_, str>) -> Result<Option<Details>, Error> {
tracing::debug!("OUTPUT: {}", output);
fn parse_details(output: DetailsOutput) -> Result<Option<Details>, Error> {
tracing::debug!("OUTPUT: {:?}", output);
let mut lines = output.lines();
let width = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let height = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let frames = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let formats = match lines.next() {
Some(line) => line,
None => return Ok(None),
};
let [stream] = output.streams;
let Format { format_name } = output.format;
for (k, v) in FORMAT_MAPPINGS {
if formats.contains(k) {
return parse_details_inner(width, height, frames, *v);
if format_name.contains(k) {
return parse_details_inner(
stream.width,
stream.height,
stream.nb_read_frames.as_deref(),
*v,
);
}
}
@ -482,14 +506,15 @@ fn parse_details(output: std::borrow::Cow<'_, str>) -> Result<Option<Details>, E
}
fn parse_details_inner(
width: &str,
height: &str,
frames: &str,
width: usize,
height: usize,
frames: Option<&str>,
format: VideoFormat,
) -> Result<Option<Details>, Error> {
let width = width.parse().map_err(|_| UploadError::UnsupportedFormat)?;
let height = height.parse().map_err(|_| UploadError::UnsupportedFormat)?;
let frames = frames.parse().map_err(|_| UploadError::UnsupportedFormat)?;
let frames = frames
.map(|frames| frames.parse().map_err(|_| UploadError::UnsupportedFormat))
.transpose()?
.unwrap_or(1);
// Probably a still image. ffmpeg thinks AVIF is an mp4
if frames == 1 {

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 1920,
"height": 1080,
"nb_read_frames": "1"
}
],
"format": {
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 160,
"height": 227,
"nb_read_frames": "28"
}
],
"format": {
"format_name": "gif"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 1920,
"height": 1080,
"nb_read_frames": "1"
}
],
"format": {
"format_name": "image2"
}
}

View file

@ -0,0 +1,14 @@
{
"programs": [
],
"streams": [
{
"width": 0,
"height": 0
}
],
"format": {
"format_name": "jpegxl_pipe"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 852,
"height": 480,
"nb_read_frames": "35364"
}
],
"format": {
"format_name": "mov,mp4,m4a,3gp,3g2,mj2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 450,
"height": 401,
"nb_read_frames": "1"
}
],
"format": {
"format_name": "png_pipe"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 112,
"height": 112,
"nb_read_frames": "27"
}
],
"format": {
"format_name": "matroska,webm"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 640,
"height": 480,
"nb_read_frames": "34650"
}
],
"format": {
"format_name": "matroska,webm"
}
}

View file

@ -0,0 +1,15 @@
{
"programs": [
],
"streams": [
{
"width": 1920,
"height": 1080,
"nb_read_frames": "1"
}
],
"format": {
"format_name": "webp_pipe"
}
}

132
src/ffmpeg/tests.rs Normal file
View file

@ -0,0 +1,132 @@
use super::{Details, DetailsOutput, PixelFormatOutput};
fn details_tests() -> [(&'static str, Option<Details>); 9] {
[
("avif", None),
(
"gif",
Some(Details {
mime_type: mime::IMAGE_GIF,
width: 160,
height: 227,
frames: Some(28),
}),
),
("jpeg", None),
("jxl", None),
(
"mp4",
Some(Details {
mime_type: crate::magick::video_mp4(),
width: 852,
height: 480,
frames: Some(35364),
}),
),
("png", None),
(
"webm",
Some(Details {
mime_type: crate::magick::video_webm(),
width: 640,
height: 480,
frames: Some(34650),
}),
),
(
"webm_av1",
Some(Details {
mime_type: crate::magick::video_webm(),
width: 112,
height: 112,
frames: Some(27),
}),
),
("webp", None),
]
}
#[test]
fn parse_details() {
for (case, expected) in details_tests() {
let string =
std::fs::read_to_string(format!("./src/ffmpeg/ffprobe_6_0_{case}_details.json"))
.expect("Read file");
let json: DetailsOutput = serde_json::from_str(&string).expect("Valid json");
let output = super::parse_details(json).expect("Parsed details");
assert_eq!(output, expected);
}
}
const ALPHA_PIXEL_FORMATS: &[&str] = &[
"pal8",
"argb",
"rgba",
"abgr",
"bgra",
"yuva420p",
"ya8",
"yuva422p",
"yuva444p",
"yuva420p9be",
"yuva420p9le",
"yuva422p9be",
"yuva422p9le",
"yuva444p9be",
"yuva444p9le",
"yuva420p10be",
"yuva420p10le",
"yuva422p10be",
"yuva422p10le",
"yuva444p10be",
"yuva444p10le",
"yuva420p16be",
"yuva420p16le",
"yuva422p16be",
"yuva422p16le",
"yuva444p16be",
"yuva444p16le",
"rgba64be",
"rgba64le",
"bgra64be",
"bgra64le",
"ya16be",
"ya16le",
"gbrap",
"gbrap16be",
"gbrap16le",
"ayuv64le",
"ayuv64be",
"gbrap12be",
"gbrap12le",
"gbrap10be",
"gbrap10le",
"gbrapf32be",
"gbrapf32le",
"yuva422p12be",
"yuva422p12le",
"yuva444p12be",
"yuva444p12le",
"vuya",
"rgbaf16be",
"rgbaf16le",
"rgbaf32be",
"rgbaf32le",
];
#[test]
fn parse_pixel_formats() {
let formats =
std::fs::read_to_string("./src/ffmpeg/ffprobe_6_0_pixel_formats.json").expect("Read file");
let json: PixelFormatOutput = serde_json::from_str(&formats).expect("Valid json");
let output = super::parse_pixel_formats(json);
for format in ALPHA_PIXEL_FORMATS {
assert!(output.contains(*format), "Doesn't contain {format}");
}
}

View file

@ -1,3 +1,6 @@
#[cfg(test)]
mod tests;
use crate::{
config::{ImageFormat, VideoCodec},
error::{Error, UploadError},
@ -123,7 +126,7 @@ impl ValidInputType {
}
}
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct Details {
pub(crate) mime_type: mime::Mime,
pub(crate) width: usize,
@ -175,18 +178,33 @@ pub(crate) async fn details_bytes(
"-".to_owned()
};
let process = Process::run(
"magick",
&["identify", "-ping", "-format", "%w %h | %m\n", &last_arg],
)?;
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])?;
let mut reader = process.bytes_read(input);
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let s = String::from_utf8_lossy(&bytes);
parse_details(s)
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&bytes)?;
parse_details(details_output)
}
#[derive(Debug, serde::Deserialize)]
struct DetailsOutput {
image: Image,
}
#[derive(Debug, serde::Deserialize)]
struct Image {
format: String,
geometry: Geometry,
}
#[derive(Debug, serde::Deserialize)]
struct Geometry {
width: usize,
height: usize,
}
#[tracing::instrument(skip(store))]
@ -217,27 +235,21 @@ pub(crate) async fn details_store<S: Store + 'static>(
"-".to_owned()
};
let process = Process::run(
"magick",
&["identify", "-ping", "-format", "%w %h | %m\n", &last_arg],
)?;
let process = Process::run("magick", &["convert", "-ping", &last_arg, "JSON:"])?;
let mut reader = process.store_read(store, identifier);
let mut output = Vec::new();
reader.read_to_end(&mut output).await?;
let s = String::from_utf8_lossy(&output);
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&output)?;
parse_details(s)
parse_details(details_output)
}
#[tracing::instrument]
pub(crate) async fn details_file(path_str: &str) -> Result<Details, Error> {
let process = Process::run(
"magick",
&["identify", "-ping", "-format", "%w %h | %m\n", path_str],
)?;
let process = Process::run("magick", &["convert", "-ping", path_str, "JSON:"])?;
let mut reader = process.read();
@ -245,46 +257,49 @@ pub(crate) async fn details_file(path_str: &str) -> Result<Details, Error> {
reader.read_to_end(&mut output).await?;
tokio::fs::remove_file(path_str).await?;
let s = String::from_utf8_lossy(&output);
let details_output: Vec<DetailsOutput> = serde_json::from_slice(&output)?;
parse_details(s)
parse_details(details_output)
}
fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
let frames = s.lines().count();
#[derive(Debug, thiserror::Error)]
pub(crate) enum ParseDetailsError {
#[error("No frames present in image")]
NoFrames,
let mut lines = s.lines();
let first = lines.next().ok_or(UploadError::UnsupportedFormat)?;
#[error("Multiple image formats used in same file")]
MixedFormats,
let mut segments = first.split('|');
#[error("Format is unsupported: {0}")]
Unsupported(String),
}
let dimensions = segments
.next()
.ok_or(UploadError::UnsupportedFormat)?
.trim();
tracing::debug!("dimensions: {}", dimensions);
let mut dims = dimensions.split(' ');
let width = dims
.next()
.ok_or(UploadError::UnsupportedFormat)?
.trim()
.parse()
.map_err(|_| UploadError::UnsupportedFormat)?;
let height = dims
.next()
.ok_or(UploadError::UnsupportedFormat)?
.trim()
.parse()
.map_err(|_| UploadError::UnsupportedFormat)?;
fn parse_details(details_output: Vec<DetailsOutput>) -> Result<Details, Error> {
let frames = details_output.len();
let format = segments
.next()
.ok_or(UploadError::UnsupportedFormat)?
.trim();
if frames == 0 {
return Err(ParseDetailsError::NoFrames.into());
}
let width = details_output
.iter()
.map(|details| details.image.geometry.width)
.max()
.expect("Nonempty vector");
let height = details_output
.iter()
.map(|details| details.image.geometry.height)
.max()
.expect("Nonempty vector");
let format = details_output[0].image.format.as_str();
tracing::debug!("format: {}", format);
if !lines.all(|item| item.ends_with(format)) {
return Err(UploadError::UnsupportedFormat.into());
if !details_output
.iter()
.all(|details| &details.image.format == format)
{
return Err(ParseDetailsError::MixedFormats.into());
}
let mime_type = match format {
@ -296,7 +311,7 @@ fn parse_details(s: std::borrow::Cow<'_, str>) -> Result<Details, Error> {
"JXL" => image_jxl(),
"PNG" => mime::IMAGE_PNG,
"WEBP" => image_webp(),
_ => return Err(UploadError::UnsupportedFormat.into()),
e => return Err(ParseDetailsError::Unsupported(String::from(e)).into()),
};
Ok(Details {
@ -368,7 +383,7 @@ impl Details {
(mime::IMAGE, subtype) if subtype.as_str() == "jxl" => ValidInputType::Jxl,
(mime::IMAGE, mime::PNG) => ValidInputType::Png,
(mime::IMAGE, subtype) if subtype.as_str() == "webp" => ValidInputType::Webp,
_ => return Err(UploadError::UnsupportedFormat.into()),
_ => return Err(ParseDetailsError::Unsupported(self.mime_type.to_string()).into()),
};
Ok(input_type)

View file

@ -0,0 +1,122 @@
[{
"version": "1.0",
"image": {
"name": "out.avif",
"baseName": "out.avif",
"permissions": 644,
"format": "AVIF",
"formatDescription": "AV1 Image File Format",
"mimeType": "image/avif",
"class": "DirectClass",
"geometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"units": "Undefined",
"type": "Bilevel",
"baseType": "Undefined",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 1,
"baseDepth": 8,
"channelDepth": {
"red": 1,
"green": 1,
"blue": 1
},
"pixels": 6220800,
"imageStatistics": {
"Overall": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"channelStatistics": {
"red": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"green": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"blue": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"compression": "Undefined",
"orientation": "TopLeft",
"properties": {
"signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"tainted": false,
"filesize": "8427B",
"numberPixels": "2.0736M",
"pixelsPerSecond": "9.82581GB",
"userTime": "0.000u",
"elapsedTime": "0:01.000",
"version": "ImageMagick 7.1.1-11 Q16-HDRI x86_64 f04a7eb33:20230528 https://imagemagick.org"
}
}]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,125 @@
[{
"version": "1.0",
"image": {
"name": "out.jpg",
"baseName": "out.jpg",
"permissions": 644,
"format": "JPEG",
"formatDescription": "Joint Photographic Experts Group JFIF format",
"mimeType": "image/jpeg",
"class": "DirectClass",
"geometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"units": "Undefined",
"type": "Bilevel",
"baseType": "Undefined",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 1,
"baseDepth": 8,
"channelDepth": {
"red": 1,
"green": 1,
"blue": 1
},
"pixels": 6220800,
"imageStatistics": {
"Overall": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"channelStatistics": {
"red": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"green": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"blue": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"compression": "JPEG",
"quality": 92,
"orientation": "Undefined",
"properties": {
"jpeg:colorspace": "2",
"jpeg:sampling-factor": "1x1,1x1,1x1",
"signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"tainted": false,
"filesize": "93100B",
"numberPixels": "2.0736M",
"pixelsPerSecond": "12.0946GB",
"userTime": "0.000u",
"elapsedTime": "0:01.000",
"version": "ImageMagick 7.1.1-11 Q16-HDRI x86_64 f04a7eb33:20230528 https://imagemagick.org"
}
}]

View file

@ -0,0 +1,122 @@
[{
"version": "1.0",
"image": {
"name": "out.jxl",
"baseName": "out.jxl",
"permissions": 644,
"format": "JXL",
"formatDescription": "JPEG XL (ISO/IEC 18181)",
"mimeType": "image/jxl",
"class": "DirectClass",
"geometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"units": "Undefined",
"type": "Bilevel",
"baseType": "Undefined",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 1,
"baseDepth": 8,
"channelDepth": {
"red": 1,
"green": 1,
"blue": 1
},
"pixels": 6220800,
"imageStatistics": {
"Overall": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"channelStatistics": {
"red": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"green": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"blue": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"compression": "Undefined",
"orientation": "TopLeft",
"properties": {
"signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"tainted": false,
"filesize": "42934B",
"numberPixels": "2.0736M",
"pixelsPerSecond": "3.79105GB",
"userTime": "0.000u",
"elapsedTime": "0:01.000",
"version": "ImageMagick 7.1.1-11 Q16-HDRI x86_64 f04a7eb33:20230528 https://imagemagick.org"
}
}]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,158 @@
[{
"version": "1.0",
"image": {
"name": "/home/asonix/Pictures/shenzi/Bad_under_Mufasa.png",
"baseName": "Bad_under_Mufasa.png",
"permissions": 644,
"format": "PNG",
"formatDescription": "Portable Network Graphics",
"mimeType": "image/png",
"class": "DirectClass",
"geometry": {
"width": 497,
"height": 694,
"x": 0,
"y": 0
},
"resolution": {
"x": 118.11,
"y": 118.11
},
"printSize": {
"x": 4.20794,
"y": 5.87588
},
"units": "PixelsPerCentimeter",
"type": "Bilevel",
"baseType": "Undefined",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 1,
"baseDepth": 8,
"channelDepth": {
"alpha": 1,
"red": 1,
"green": 1,
"blue": 1
},
"pixels": 1379672,
"imageStatistics": {
"Overall": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"channelStatistics": {
"alpha": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"red": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"green": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
},
"blue": {
"min": 0,
"max": 0,
"mean": 0,
"median": 0,
"standardDeviation": 0,
"kurtosis": -3,
"skewness": 0,
"entropy": 0
}
},
"alpha": "#00000000",
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 497,
"height": 694,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"compression": "Zip",
"orientation": "Undefined",
"properties": {
"icc:copyright": "Public Domain",
"icc:description": "Glimpse built-in sRGB",
"icc:manufacturer": "GIMP",
"icc:model": "sRGB",
"png:IHDR.bit-depth-orig": "8",
"png:IHDR.bit_depth": "8",
"png:IHDR.color-type-orig": "6",
"png:IHDR.color_type": "6 (RGBA)",
"png:IHDR.interlace_method": "0 (Not interlaced)",
"png:IHDR.width,height": "497, 694",
"png:tIME": "2020-09-03T00:14:24Z",
"signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"profiles": {
"icc": {
"length": 680
}
},
"tainted": false,
"filesize": "269655B",
"numberPixels": "344918",
"pixelsPerSecond": "1.13565GB",
"userTime": "0.000u",
"elapsedTime": "0:01.000",
"version": "ImageMagick 7.1.1-11 Q16-HDRI x86_64 f04a7eb33:20230528 https://imagemagick.org"
}
}]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,122 @@
[{
"version": "1.0",
"image": {
"name": "/home/asonix/Pictures/shenzi/1242635_1372997258512_full.webp",
"baseName": "1242635_1372997258512_full.webp",
"permissions": 644,
"format": "WEBP",
"formatDescription": "WebP Image Format",
"mimeType": "image/webp",
"class": "DirectClass",
"geometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"units": "Undefined",
"type": "Bilevel",
"baseType": "Undefined",
"endianness": "Undefined",
"colorspace": "sRGB",
"depth": 1,
"baseDepth": 8,
"channelDepth": {
"red": 8,
"green": 8,
"blue": 1
},
"pixels": 6220800,
"imageStatistics": {
"Overall": {
"min": 0,
"max": 244,
"mean": 99.8391,
"median": 121.333,
"standardDeviation": 43.645,
"kurtosis": -0.145553,
"skewness": 0.984726,
"entropy": 0.701104
}
},
"channelStatistics": {
"red": {
"min": 34,
"max": 244,
"mean": 166.595,
"median": 196,
"standardDeviation": 70.8386,
"kurtosis": -1.65286,
"skewness": -0.357383,
"entropy": 0.725609
},
"green": {
"min": 0,
"max": 106,
"mean": 70.0406,
"median": 88,
"standardDeviation": 30.5371,
"kurtosis": -1.49345,
"skewness": -0.455794,
"entropy": 0.704115
},
"blue": {
"min": 0,
"max": 100,
"mean": 62.8818,
"median": 80,
"standardDeviation": 29.5594,
"kurtosis": -1.51232,
"skewness": -0.389386,
"entropy": 0.673587
}
},
"renderingIntent": "Perceptual",
"gamma": 0.454545,
"chromaticity": {
"redPrimary": {
"x": 0.64,
"y": 0.33
},
"greenPrimary": {
"x": 0.3,
"y": 0.6
},
"bluePrimary": {
"x": 0.15,
"y": 0.06
},
"whitePrimary": {
"x": 0.3127,
"y": 0.329
}
},
"matteColor": "#BDBDBDBDBDBD",
"backgroundColor": "#FFFFFFFFFFFF",
"borderColor": "#DFDFDFDFDFDF",
"transparentColor": "#000000000000",
"interlace": "None",
"intensity": "Undefined",
"compose": "Over",
"pageGeometry": {
"width": 1920,
"height": 1080,
"x": 0,
"y": 0
},
"dispose": "Undefined",
"iterations": 0,
"compression": "Undefined",
"orientation": "Undefined",
"properties": {
"signature": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
"tainted": false,
"filesize": "51106B",
"numberPixels": "2.0736M",
"pixelsPerSecond": "23.1706MB",
"userTime": "0.080u",
"elapsedTime": "0:01.089",
"version": "ImageMagick 7.1.1-11 Q16-HDRI x86_64 f04a7eb33:20230528 https://imagemagick.org"
}
}]

93
src/magick/tests.rs Normal file
View file

@ -0,0 +1,93 @@
use super::{Details, DetailsOutput};
fn details_tests() -> [(&'static str, Details); 8] {
[
(
"avif",
Details {
mime_type: super::image_avif(),
width: 1920,
height: 1080,
frames: None,
},
),
(
"gif",
Details {
mime_type: mime::IMAGE_GIF,
width: 414,
height: 261,
frames: Some(17),
},
),
(
"jpeg",
Details {
mime_type: mime::IMAGE_JPEG,
width: 1920,
height: 1080,
frames: None,
},
),
(
"jxl",
Details {
mime_type: super::image_jxl(),
width: 1920,
height: 1080,
frames: None,
},
),
(
"mp4",
Details {
mime_type: super::video_mp4(),
width: 414,
height: 261,
frames: Some(17),
},
),
(
"png",
Details {
mime_type: mime::IMAGE_PNG,
width: 497,
height: 694,
frames: None,
},
),
(
"webm",
Details {
mime_type: super::video_webm(),
width: 112,
height: 112,
frames: Some(27),
},
),
(
"webp",
Details {
mime_type: super::image_webp(),
width: 1920,
height: 1080,
frames: None,
},
),
]
}
#[test]
fn parse_details() {
for (case, expected) in details_tests() {
let string =
std::fs::read_to_string(format!("./src/magick/magick_7_1_1_{case}_details.json"))
.expect("Read file");
let json: Vec<DetailsOutput> = serde_json::from_str(&string).expect("Valid json");
let output = super::parse_details(json).expect("Parsed details");
assert_eq!(output, expected);
}
}