commit cf9c084cba8283aa90a5943ed0affc5955cce04f Author: kageru Date: Thu May 30 11:06:53 2019 +0000 initial commit diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..56492e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "adaptivegrain-rs" +version = "0.1.0" +authors = ["kageru"] +edition = "2018" + +[dependencies] +failure = "0.1" +vapoursynth = { path = "vapoursynth" } + +[lib] +crate-type = ["cdylib"] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5e994ab --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,95 @@ +#[macro_use] +extern crate failure; +#[macro_use] +extern crate vapoursynth; + +use vapoursynth::core::CoreRef; +use vapoursynth::map::Map; +use vapoursynth::video_info::VideoInfo; +use failure::Error; +use vapoursynth::node::Node; +use vapoursynth::plugins::{Filter, FilterArgument, FrameContext, Metadata}; +use vapoursynth::api::API; +use vapoursynth::frame::{FrameRef, FrameRefMut}; + +const PLUGIN_NAME: &str = "adaptivegrain"; +const PLUGIN_IDENTIFIER: &str = "moe.kageru.adaptivegrain"; + +struct AdaptiveGrain<'core> { + source: Node<'core> +} + +impl<'core> Filter<'core> for AdaptiveGrain<'core> { + fn video_info(&self, _api: API, _core: CoreRef<'core>) -> Vec> { + vec![self.source.info()] + } + + fn get_frame_initial( + &self, + _api: API, + _core: CoreRef<'core>, + context: FrameContext, + n: usize, + ) -> Result>, Error> { + self.source.request_frame_filter(context, n); + Ok(None) + } + + fn get_frame( + &self, + _api: API, + core: CoreRef<'core>, + context: FrameContext, + n: usize, + ) -> Result, Error> { + let frame = self.source + .get_frame_filter(context, n) + .ok_or_else(|| format_err!("Could not retrieve source frame. This shouldn’t happen."))?; + let average = match frame.props().get::("PlaneStatsAverage") { + Ok(average) => (average * 256.0) as u8, + Err(_) => panic!(PLUGIN_NAME.to_owned() + ": You need to run std.PlaneStats on the clip before calling this function.") + }; + + let mut frame = FrameRefMut::copy_of(core, &frame); + for plane in 0..frame.format().plane_count() { + for row in 0..frame.height(plane) { + for pixel in frame.plane_row_mut::(plane, row) { + *pixel = average; + } + } + } + Ok(frame.into()) + } +} + +make_filter_function! { + AdaptiveGrainFunction, "AdaptiveGrain" + + fn create_adaptivegrain<'core>( + _api: API, + _core: CoreRef<'core>, + clip: Node<'core>, + ) -> Result + 'core>>, Error> { + Ok(Some(Box::new(AdaptiveGrain { source: clip }))) + } +} + +export_vapoursynth_plugin! { + Metadata { + identifier: PLUGIN_IDENTIFIER, + namespace: "adg", + name: "Adaptive grain", + read_only: false, + }, + [ + AdaptiveGrainFunction::new() + ] +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/vapoursynth-sys/.gitmodules b/vapoursynth-sys/.gitmodules new file mode 100644 index 0000000..6bcc3fe --- /dev/null +++ b/vapoursynth-sys/.gitmodules @@ -0,0 +1,3 @@ +[submodule "source-files/vapoursynth"] + path = source-files/vapoursynth + url = https://github.com/vapoursynth/vapoursynth.git diff --git a/vapoursynth-sys/CHANGELOG.md b/vapoursynth-sys/CHANGELOG.md new file mode 100644 index 0000000..0c652e7 --- /dev/null +++ b/vapoursynth-sys/CHANGELOG.md @@ -0,0 +1,24 @@ +### v0.2.1 (16th Jun 2018) +- Added missing plugin-related types +- Silenced some clippy warnings + +## v0.2.0 (24th Mar 2018) +- Added support for targetting 32-bit Windows +- Added automatic detection of common Windows VapourSynth library dirs + +### v0.1.4 (11th Mar 2018) +- Added an AppVeyor badge +- Added a link to vapoursynth-rs in the README + +### v0.1.3 (18th Feb 2018) +- Added a docs.rs badge to the README +- Added CHANGELOG.md + +### v0.1.2 (18th Feb 2018) +- Changed the documentation to be hosted at docs.rs + +### v0.1.1 (2nd Feb 2018) +- Added new bindings from VSScript API 3.2 + +## v0.1.0 +- Initial release diff --git a/vapoursynth-sys/Cargo.toml b/vapoursynth-sys/Cargo.toml new file mode 100644 index 0000000..c7a442b --- /dev/null +++ b/vapoursynth-sys/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "vapoursynth-sys" +version = "0.2.2" # remember to update html_root_url +authors = ["Ivan Molodetskikh "] +description = "Rust bindings for vapoursynth and vsscript." +license = "MIT/Apache-2.0" +build = "build.rs" + +readme = "README.md" +documentation = "https://docs.rs/vapoursynth-sys" +repository = "https://github.com/YaLTeR/vapoursynth-rs" +keywords = ["vapoursynth", "vsscript", "video", "bindings"] +categories = ["api-bindings", "external-ffi-bindings", "multimedia::video"] + +[badges] +travis-ci = { repository = "YaLTeR/vapoursynth-rs" } +appveyor = { repository = "YaLTeR/vapoursynth-rs" } + +[dependencies] +cfg-if = "0.1" + +[features] +# Features for enabling higher API versions. +vapoursynth-api-31 = ["gte-vapoursynth-api-31"] +vapoursynth-api-32 = ["gte-vapoursynth-api-31", "gte-vapoursynth-api-32"] +vapoursynth-api-33 = ["gte-vapoursynth-api-31", "gte-vapoursynth-api-32", "gte-vapoursynth-api-33"] +vapoursynth-api-34 = [ + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32", + "gte-vapoursynth-api-33", + "gte-vapoursynth-api-34" +] +vapoursynth-api-35 = [ + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32", + "gte-vapoursynth-api-33", + "gte-vapoursynth-api-34", + "gte-vapoursynth-api-35" +] + +vsscript-api-31 = ["gte-vsscript-api-31"] +vsscript-api-32 = ["gte-vsscript-api-31", "gte-vsscript-api-32"] + +# Features for linking to certain functions. +vapoursynth-functions = [] +vsscript-functions = [] + +# Utility features, not for outside use. +gte-vapoursynth-api-31 = [] +gte-vapoursynth-api-32 = [] +gte-vapoursynth-api-33 = [] +gte-vapoursynth-api-34 = [] +gte-vapoursynth-api-35 = [] + +gte-vsscript-api-31 = [] +gte-vsscript-api-32 = [] + +[package.metadata.docs.rs] +features = ["vapoursynth-api-35", "vsscript-api-32", "vapoursynth-functions", "vsscript-functions"] diff --git a/vapoursynth-sys/LICENSE-APACHE b/vapoursynth-sys/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/vapoursynth-sys/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/vapoursynth-sys/LICENSE-MIT b/vapoursynth-sys/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/vapoursynth-sys/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/vapoursynth-sys/README.md b/vapoursynth-sys/README.md new file mode 100644 index 0000000..0527526 --- /dev/null +++ b/vapoursynth-sys/README.md @@ -0,0 +1,43 @@ +# vapoursynth-sys + +[![crates.io](https://img.shields.io/crates/v/vapoursynth-sys.svg)](https://crates.io/crates/vapoursynth-sys) +[![Documentation](https://docs.rs/vapoursynth-sys/badge.svg)](https://docs.rs/vapoursynth-sys) + +[ChangeLog](https://github.com/YaLTeR/vapoursynth-rs/blob/master/vapoursynth-sys/CHANGELOG.md) + +Raw bindings to [VapourSynth](https://github.com/vapoursynth/vapoursynth). + +Check out [vapoursynth-rs](https://crates.io/crates/vapoursynth) for a safe Rust wrapper. + +## Supported Versions + +All VapourSynth and VSScript API versions starting with 3.0 are supported. By default the crates use the 3.0 feature set. To enable higher API version support, enable one of the following Cargo features: + +* `vapoursynth-api-31` for VapourSynth API 3.1 +* `vapoursynth-api-32` for VapourSynth API 3.2 +* `vapoursynth-api-33` for VapourSynth API 3.3 +* `vapoursynth-api-34` for VapourSynth API 3.4 +* `vapoursynth-api-35` for VapourSynth API 3.5 +* `vsscript-api-31` for VSScript API 3.1 +* `vsscript-api-32` for VSScript API 3.2 + +To enable linking to VapourSynth or VSScript functions, enable the following Cargo features: + +* `vapoursynth-functions` for VapourSynth functions (`getVapourSynthAPI()`) +* `vsscript-functions` for VSScript functions (`vsscript_*()`) + +## Building + +Make sure you have the corresponding libraries available if you enable the linking features. You can use the `VAPOURSYNTH_LIB_DIR` environment variable to specify a custom directory with the library files. + +On Windows the easiest way is to use the VapourSynth installer (make sure the VapourSynth SDK is checked). The crate should pick up the library directory automatically. If it doesn't or if you're cross-compiling, set `VAPOURSYNTH_LIB_DIR` to `\sdk\lib64` or `<...>\lib32`, depending on the target bitness. + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + diff --git a/vapoursynth-sys/build.rs b/vapoursynth-sys/build.rs new file mode 100644 index 0000000..c633ae8 --- /dev/null +++ b/vapoursynth-sys/build.rs @@ -0,0 +1,67 @@ +use std::env; +use std::path::PathBuf; + +const LIBRARY_DIR_VARIABLE: &str = "VAPOURSYNTH_LIB_DIR"; + +fn main() { + // Make sure the build script is re-run if our env variable is changed. + println!("cargo:rerun-if-env-changed={}", LIBRARY_DIR_VARIABLE); + + let windows = env::var("TARGET").unwrap().contains("windows"); + + // Get the default library dir on Windows. + let default_library_dir = if windows { + get_default_library_dir() + } else { + None + }; + + // Library directory override or the default dir on windows. + if let Some(dir) = env::var(LIBRARY_DIR_VARIABLE).ok().or(default_library_dir) { + println!("cargo:rustc-link-search=native={}", dir); + } + + // Handle linking to VapourSynth libs. + if env::var("CARGO_FEATURE_VAPOURSYNTH_FUNCTIONS").is_ok() { + println!("cargo:rustc-link-lib=vapoursynth"); + } + + if env::var("CARGO_FEATURE_VSSCRIPT_FUNCTIONS").is_ok() { + let vsscript_lib_name = if windows { + "vsscript" + } else { + "vapoursynth-script" + }; + + println!("cargo:rustc-link-lib={}", vsscript_lib_name); + } +} + +// Returns the default library dir on Windows. +// The default dir is where the VapourSynth installer puts the libraries. +fn get_default_library_dir() -> Option { + let host = env::var("HOST").ok()?; + + // If the host isn't Windows we don't have %programfiles%. + if !host.contains("windows") { + return None; + } + + let programfiles = if host.starts_with("i686") { + env::var("programfiles") + } else { + env::var("programfiles(x86)") + }; + + let suffix = if env::var("TARGET").ok()?.starts_with("i686") { + "lib32" + } else { + "lib64" + }; + + let mut path = PathBuf::from(programfiles.ok()?); + path.push("VapourSynth"); + path.push("sdk"); + path.push(suffix); + path.to_str().map(|s| s.to_owned()) +} diff --git a/vapoursynth-sys/source-files/generate-bindings.sh b/vapoursynth-sys/source-files/generate-bindings.sh new file mode 100755 index 0000000..e0b028f --- /dev/null +++ b/vapoursynth-sys/source-files/generate-bindings.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +bindgen --whitelist-function 'getVapourSynthAPI' \ + --whitelist-function 'vsscript.*' \ + --whitelist-type 'VSColorFamily' \ + --whitelist-type 'VSSampleType' \ + --whitelist-type 'VSPresetFormat' \ + --whitelist-type 'VSFilterMode' \ + --whitelist-type 'VSNodeFlags' \ + --whitelist-type 'VSPropTypes' \ + --whitelist-type 'VSGetPropErrors' \ + --whitelist-type 'VSPropAppendMode' \ + --whitelist-type 'VSActivationReason' \ + --whitelist-type 'VSMessageType' \ + --whitelist-type 'VSEvalFlags' \ + --whitelist-type 'VSInitPlugin' \ + --blacklist-type '__int64_t' \ + --blacklist-type '__uint8_t' \ + --bitfield-enum 'VSNodeFlags' \ + --rustified-enum 'VS[^N].*' \ + --no-layout-tests \ + -o bindings.rs \ + vapoursynth/include/VSScript.h \ + -- -target x86_64-unknown-windows-unknown diff --git a/vapoursynth-sys/source-files/wrapper.h b/vapoursynth-sys/source-files/wrapper.h new file mode 100644 index 0000000..08ca681 --- /dev/null +++ b/vapoursynth-sys/source-files/wrapper.h @@ -0,0 +1 @@ +#include "vapoursynth/include/VSScript.h" diff --git a/vapoursynth-sys/src/bindings.rs b/vapoursynth-sys/src/bindings.rs new file mode 100644 index 0000000..9039dcd --- /dev/null +++ b/vapoursynth-sys/src/bindings.rs @@ -0,0 +1,564 @@ +use std::os::raw::*; + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSFrameRef { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSNodeRef { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSCore { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSPlugin { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSNode { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSFuncRef { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSMap { + _unused: [u8; 0], +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSFrameContext { + _unused: [u8; 0], +} +#[cfg_attr(feature = "cargo-clippy", allow(unreadable_literal))] +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSColorFamily { + cmGray = 1000000, + cmRGB = 2000000, + cmYUV = 3000000, + cmYCoCg = 4000000, + cmCompat = 9000000, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSSampleType { + stInteger = 0, + stFloat = 1, +} +#[cfg_attr(feature = "cargo-clippy", allow(unreadable_literal))] +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSPresetFormat { + pfNone = 0, + pfGray8 = 1000010, + pfGray16 = 1000011, + pfGrayH = 1000012, + pfGrayS = 1000013, + pfYUV420P8 = 3000010, + pfYUV422P8 = 3000011, + pfYUV444P8 = 3000012, + pfYUV410P8 = 3000013, + pfYUV411P8 = 3000014, + pfYUV440P8 = 3000015, + pfYUV420P9 = 3000016, + pfYUV422P9 = 3000017, + pfYUV444P9 = 3000018, + pfYUV420P10 = 3000019, + pfYUV422P10 = 3000020, + pfYUV444P10 = 3000021, + pfYUV420P16 = 3000022, + pfYUV422P16 = 3000023, + pfYUV444P16 = 3000024, + pfYUV444PH = 3000025, + pfYUV444PS = 3000026, + pfYUV420P12 = 3000027, + pfYUV422P12 = 3000028, + pfYUV444P12 = 3000029, + pfYUV420P14 = 3000030, + pfYUV422P14 = 3000031, + pfYUV444P14 = 3000032, + pfRGB24 = 2000010, + pfRGB27 = 2000011, + pfRGB30 = 2000012, + pfRGB48 = 2000013, + pfRGBH = 2000014, + pfRGBS = 2000015, + pfCompatBGR32 = 9000010, + pfCompatYUY2 = 9000011, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSFilterMode { + fmParallel = 100, + fmParallelRequests = 200, + fmUnordered = 300, + fmSerial = 400, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSFormat { + pub name: [c_char; 32usize], + pub id: c_int, + pub colorFamily: c_int, + pub sampleType: c_int, + pub bitsPerSample: c_int, + pub bytesPerSample: c_int, + pub subSamplingW: c_int, + pub subSamplingH: c_int, + pub numPlanes: c_int, +} +pub const VSNodeFlags_nfNoCache: VSNodeFlags = VSNodeFlags(1); +pub const VSNodeFlags_nfIsCache: VSNodeFlags = VSNodeFlags(2); +#[cfg(feature = "gte-vapoursynth-api-33")] +pub const VSNodeFlags_nfMakeLinear: VSNodeFlags = VSNodeFlags(4); +impl ::std::ops::BitOr for VSNodeFlags { + type Output = Self; + #[inline] + fn bitor(self, other: Self) -> Self { + VSNodeFlags(self.0 | other.0) + } +} +impl ::std::ops::BitOrAssign for VSNodeFlags { + #[inline] + fn bitor_assign(&mut self, rhs: VSNodeFlags) { + self.0 |= rhs.0; + } +} +impl ::std::ops::BitAnd for VSNodeFlags { + type Output = Self; + #[inline] + fn bitand(self, other: Self) -> Self { + VSNodeFlags(self.0 & other.0) + } +} +impl ::std::ops::BitAndAssign for VSNodeFlags { + #[inline] + fn bitand_assign(&mut self, rhs: VSNodeFlags) { + self.0 &= rhs.0; + } +} +#[repr(C)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct VSNodeFlags(pub c_int); +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSPropTypes { + ptUnset = 117, + ptInt = 105, + ptFloat = 102, + ptData = 115, + ptNode = 99, + ptFrame = 118, + ptFunction = 109, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSGetPropErrors { + peUnset = 1, + peType = 2, + peIndex = 4, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSPropAppendMode { + paReplace = 0, + paAppend = 1, + paTouch = 2, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSCoreInfo { + pub versionString: *const c_char, + pub core: c_int, + pub api: c_int, + pub numThreads: c_int, + pub maxFramebufferSize: i64, + pub usedFramebufferSize: i64, +} +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSVideoInfo { + pub format: *const VSFormat, + pub fpsNum: i64, + pub fpsDen: i64, + pub width: c_int, + pub height: c_int, + pub numFrames: c_int, + pub flags: c_int, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSActivationReason { + arInitial = 0, + arFrameReady = 1, + arAllFramesReady = 2, + arError = -1, +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSMessageType { + mtDebug = 0, + mtWarning = 1, + mtCritical = 2, + mtFatal = 3, +} +pub type VSPublicFunction = unsafe extern "system" fn( + in_: *const VSMap, + out: *mut VSMap, + userData: *mut c_void, + core: *mut VSCore, + vsapi: *const VSAPI, +); +pub type VSRegisterFunction = unsafe extern "system" fn( + name: *const c_char, + args: *const c_char, + argsFunc: VSPublicFunction, + functionData: *mut c_void, + plugin: *mut VSPlugin, +); +pub type VSConfigPlugin = unsafe extern "system" fn( + identifier: *const c_char, + defaultNamespace: *const c_char, + name: *const c_char, + apiVersion: c_int, + readonly: c_int, + plugin: *mut VSPlugin, +); +pub type VSInitPlugin = Option< + unsafe extern "system" fn( + configFunc: VSConfigPlugin, + registerFunc: VSRegisterFunction, + plugin: *mut VSPlugin, + ), +>; +pub type VSFreeFuncData = Option; +pub type VSFilterInit = unsafe extern "system" fn( + in_: *mut VSMap, + out: *mut VSMap, + instanceData: *mut *mut c_void, + node: *mut VSNode, + core: *mut VSCore, + vsapi: *const VSAPI, +); +pub type VSFilterGetFrame = unsafe extern "system" fn( + n: c_int, + activationReason: c_int, + instanceData: *mut *mut c_void, + frameData: *mut *mut c_void, + frameCtx: *mut VSFrameContext, + core: *mut VSCore, + vsapi: *const VSAPI, +) -> *const VSFrameRef; +pub type VSFilterFree = Option< + unsafe extern "system" fn(instanceData: *mut c_void, core: *mut VSCore, vsapi: *const VSAPI), +>; +pub type VSFrameDoneCallback = Option< + unsafe extern "system" fn( + userData: *mut c_void, + f: *const VSFrameRef, + n: c_int, + arg1: *mut VSNodeRef, + errorMsg: *const c_char, + ), +>; +pub type VSMessageHandler = + Option; +#[repr(C)] +#[derive(Copy, Clone)] +pub struct VSAPI { + pub createCore: unsafe extern "system" fn(threads: c_int) -> *mut VSCore, + pub freeCore: unsafe extern "system" fn(core: *mut VSCore), + pub getCoreInfo: unsafe extern "system" fn(core: *mut VSCore) -> *const VSCoreInfo, + pub cloneFrameRef: unsafe extern "system" fn(f: *const VSFrameRef) -> *const VSFrameRef, + pub cloneNodeRef: unsafe extern "system" fn(node: *mut VSNodeRef) -> *mut VSNodeRef, + pub cloneFuncRef: unsafe extern "system" fn(f: *mut VSFuncRef) -> *mut VSFuncRef, + pub freeFrame: unsafe extern "system" fn(f: *const VSFrameRef), + pub freeNode: unsafe extern "system" fn(node: *mut VSNodeRef), + pub freeFunc: unsafe extern "system" fn(f: *mut VSFuncRef), + pub newVideoFrame: unsafe extern "system" fn( + format: *const VSFormat, + width: c_int, + height: c_int, + propSrc: *const VSFrameRef, + core: *mut VSCore, + ) -> *mut VSFrameRef, + pub copyFrame: + unsafe extern "system" fn(f: *const VSFrameRef, core: *mut VSCore) -> *mut VSFrameRef, + pub copyFrameProps: + unsafe extern "system" fn(src: *const VSFrameRef, dst: *mut VSFrameRef, core: *mut VSCore), + pub registerFunction: unsafe extern "system" fn( + name: *const c_char, + args: *const c_char, + argsFunc: VSPublicFunction, + functionData: *mut c_void, + plugin: *mut VSPlugin, + ), + pub getPluginById: + unsafe extern "system" fn(identifier: *const c_char, core: *mut VSCore) -> *mut VSPlugin, + pub getPluginByNs: + unsafe extern "system" fn(ns: *const c_char, core: *mut VSCore) -> *mut VSPlugin, + pub getPlugins: unsafe extern "system" fn(core: *mut VSCore) -> *mut VSMap, + pub getFunctions: unsafe extern "system" fn(plugin: *mut VSPlugin) -> *mut VSMap, + #[cfg_attr(feature = "cargo-clippy", allow(type_complexity))] + pub createFilter: unsafe extern "system" fn( + in_: *const VSMap, + out: *mut VSMap, + name: *const c_char, + init: VSFilterInit, + getFrame: VSFilterGetFrame, + free: VSFilterFree, + filterMode: c_int, + flags: c_int, + instanceData: *mut c_void, + core: *mut VSCore, + ), + pub setError: unsafe extern "system" fn(map: *mut VSMap, errorMessage: *const c_char), + pub getError: unsafe extern "system" fn(map: *const VSMap) -> *const c_char, + pub setFilterError: + unsafe extern "system" fn(errorMessage: *const c_char, frameCtx: *mut VSFrameContext), + pub invoke: + unsafe extern "system" fn(plugin: *mut VSPlugin, name: *const c_char, args: *const VSMap) + -> *mut VSMap, + pub getFormatPreset: unsafe extern "system" fn(id: c_int, core: *mut VSCore) -> *const VSFormat, + pub registerFormat: unsafe extern "system" fn( + colorFamily: c_int, + sampleType: c_int, + bitsPerSample: c_int, + subSamplingW: c_int, + subSamplingH: c_int, + core: *mut VSCore, + ) -> *const VSFormat, + pub getFrame: unsafe extern "system" fn( + n: c_int, + node: *mut VSNodeRef, + errorMsg: *mut c_char, + bufSize: c_int, + ) -> *const VSFrameRef, + pub getFrameAsync: unsafe extern "system" fn( + n: c_int, + node: *mut VSNodeRef, + callback: VSFrameDoneCallback, + userData: *mut c_void, + ), + pub getFrameFilter: + unsafe extern "system" fn(n: c_int, node: *mut VSNodeRef, frameCtx: *mut VSFrameContext) + -> *const VSFrameRef, + pub requestFrameFilter: + unsafe extern "system" fn(n: c_int, node: *mut VSNodeRef, frameCtx: *mut VSFrameContext), + pub queryCompletedFrame: unsafe extern "system" fn( + node: *mut *mut VSNodeRef, + n: *mut c_int, + frameCtx: *mut VSFrameContext, + ), + pub releaseFrameEarly: + unsafe extern "system" fn(node: *mut VSNodeRef, n: c_int, frameCtx: *mut VSFrameContext), + pub getStride: unsafe extern "system" fn(f: *const VSFrameRef, plane: c_int) -> c_int, + pub getReadPtr: unsafe extern "system" fn(f: *const VSFrameRef, plane: c_int) -> *const u8, + pub getWritePtr: unsafe extern "system" fn(f: *mut VSFrameRef, plane: c_int) -> *mut u8, + pub createFunc: unsafe extern "system" fn( + func: VSPublicFunction, + userData: *mut c_void, + free: VSFreeFuncData, + core: *mut VSCore, + vsapi: *const VSAPI, + ) -> *mut VSFuncRef, + pub callFunc: unsafe extern "system" fn( + func: *mut VSFuncRef, + in_: *const VSMap, + out: *mut VSMap, + core: *mut VSCore, + vsapi: *const VSAPI, + ), + pub createMap: unsafe extern "system" fn() -> *mut VSMap, + pub freeMap: unsafe extern "system" fn(map: *mut VSMap), + pub clearMap: unsafe extern "system" fn(map: *mut VSMap), + pub getVideoInfo: unsafe extern "system" fn(node: *mut VSNodeRef) -> *const VSVideoInfo, + pub setVideoInfo: + unsafe extern "system" fn(vi: *const VSVideoInfo, numOutputs: c_int, node: *mut VSNode), + pub getFrameFormat: unsafe extern "system" fn(f: *const VSFrameRef) -> *const VSFormat, + pub getFrameWidth: unsafe extern "system" fn(f: *const VSFrameRef, plane: c_int) -> c_int, + pub getFrameHeight: unsafe extern "system" fn(f: *const VSFrameRef, plane: c_int) -> c_int, + pub getFramePropsRO: unsafe extern "system" fn(f: *const VSFrameRef) -> *const VSMap, + pub getFramePropsRW: unsafe extern "system" fn(f: *mut VSFrameRef) -> *mut VSMap, + pub propNumKeys: unsafe extern "system" fn(map: *const VSMap) -> c_int, + pub propGetKey: unsafe extern "system" fn(map: *const VSMap, index: c_int) -> *const c_char, + pub propNumElements: unsafe extern "system" fn(map: *const VSMap, key: *const c_char) -> c_int, + pub propGetType: unsafe extern "system" fn(map: *const VSMap, key: *const c_char) -> c_char, + pub propGetInt: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> i64, + pub propGetFloat: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> f64, + pub propGetData: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> *const c_char, + pub propGetDataSize: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> c_int, + pub propGetNode: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> *mut VSNodeRef, + pub propGetFrame: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> *const VSFrameRef, + pub propGetFunc: unsafe extern "system" fn( + map: *const VSMap, + key: *const c_char, + index: c_int, + error: *mut c_int, + ) -> *mut VSFuncRef, + pub propDeleteKey: unsafe extern "system" fn(map: *mut VSMap, key: *const c_char) -> c_int, + pub propSetInt: + unsafe extern "system" fn(map: *mut VSMap, key: *const c_char, i: i64, append: c_int) + -> c_int, + pub propSetFloat: + unsafe extern "system" fn(map: *mut VSMap, key: *const c_char, d: f64, append: c_int) + -> c_int, + pub propSetData: unsafe extern "system" fn( + map: *mut VSMap, + key: *const c_char, + data: *const c_char, + size: c_int, + append: c_int, + ) -> c_int, + pub propSetNode: unsafe extern "system" fn( + map: *mut VSMap, + key: *const c_char, + node: *mut VSNodeRef, + append: c_int, + ) -> c_int, + pub propSetFrame: unsafe extern "system" fn( + map: *mut VSMap, + key: *const c_char, + f: *const VSFrameRef, + append: c_int, + ) -> c_int, + pub propSetFunc: unsafe extern "system" fn( + map: *mut VSMap, + key: *const c_char, + func: *mut VSFuncRef, + append: c_int, + ) -> c_int, + pub setMaxCacheSize: unsafe extern "system" fn(bytes: i64, core: *mut VSCore) -> i64, + pub getOutputIndex: unsafe extern "system" fn(frameCtx: *mut VSFrameContext) -> c_int, + pub newVideoFrame2: unsafe extern "system" fn( + format: *const VSFormat, + width: c_int, + height: c_int, + planeSrc: *mut *const VSFrameRef, + planes: *const c_int, + propSrc: *const VSFrameRef, + core: *mut VSCore, + ) -> *mut VSFrameRef, + pub setMessageHandler: + unsafe extern "system" fn(handler: VSMessageHandler, userData: *mut c_void), + pub setThreadCount: unsafe extern "system" fn(threads: c_int, core: *mut VSCore) -> c_int, + pub getPluginPath: unsafe extern "system" fn(plugin: *const VSPlugin) -> *const c_char, + + #[cfg(feature = "gte-vapoursynth-api-31")] + pub propGetIntArray: + unsafe extern "system" fn(map: *const VSMap, key: *const c_char, error: *mut c_int) + -> *const i64, + #[cfg(feature = "gte-vapoursynth-api-31")] + pub propGetFloatArray: + unsafe extern "system" fn(map: *const VSMap, key: *const c_char, error: *mut c_int) + -> *const f64, + #[cfg(feature = "gte-vapoursynth-api-31")] + pub propSetIntArray: + unsafe extern "system" fn(map: *mut VSMap, key: *const c_char, i: *const i64, size: c_int) + -> c_int, + #[cfg(feature = "gte-vapoursynth-api-31")] + pub propSetFloatArray: + unsafe extern "system" fn(map: *mut VSMap, key: *const c_char, d: *const f64, size: c_int) + -> c_int, + #[cfg(feature = "gte-vapoursynth-api-34")] + pub logMessage: unsafe extern "system" fn(msgType: c_int, msg: *const c_char), +} + +#[cfg(feature = "vapoursynth-functions")] +extern "system" { + pub fn getVapourSynthAPI(version: c_int) -> *const VSAPI; +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct VSScript { + _unused: [u8; 0], +} +#[repr(i32)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum VSEvalFlags { + efSetWorkingDir = 1, +} + +#[cfg(feature = "vsscript-functions")] +extern "system" { + #[cfg(feature = "gte-vsscript-api-31")] + pub fn vsscript_getApiVersion() -> c_int; + pub fn vsscript_init() -> c_int; + pub fn vsscript_finalize() -> c_int; + pub fn vsscript_evaluateScript( + handle: *mut *mut VSScript, + script: *const c_char, + scriptFilename: *const c_char, + flags: c_int, + ) -> c_int; + pub fn vsscript_evaluateFile( + handle: *mut *mut VSScript, + scriptFilename: *const c_char, + flags: c_int, + ) -> c_int; + pub fn vsscript_createScript(handle: *mut *mut VSScript) -> c_int; + pub fn vsscript_freeScript(handle: *mut VSScript); + pub fn vsscript_getError(handle: *mut VSScript) -> *const c_char; + pub fn vsscript_getOutput(handle: *mut VSScript, index: c_int) -> *mut VSNodeRef; + #[cfg(feature = "gte-vsscript-api-31")] + pub fn vsscript_getOutput2( + handle: *mut VSScript, + index: c_int, + alpha: *mut *mut VSNodeRef, + ) -> *mut VSNodeRef; + pub fn vsscript_clearOutput(handle: *mut VSScript, index: c_int) -> c_int; + pub fn vsscript_getCore(handle: *mut VSScript) -> *mut VSCore; + pub fn vsscript_getVSApi() -> *const VSAPI; + #[cfg(feature = "gte-vsscript-api-32")] + pub fn vsscript_getVSApi2(version: c_int) -> *const VSAPI; + pub fn vsscript_getVariable( + handle: *mut VSScript, + name: *const c_char, + dst: *mut VSMap, + ) -> c_int; + pub fn vsscript_setVariable(handle: *mut VSScript, vars: *const VSMap) -> c_int; + pub fn vsscript_clearVariable(handle: *mut VSScript, name: *const c_char) -> c_int; + pub fn vsscript_clearEnvironment(handle: *mut VSScript); +} diff --git a/vapoursynth-sys/src/lib.rs b/vapoursynth-sys/src/lib.rs new file mode 100644 index 0000000..2c1024f --- /dev/null +++ b/vapoursynth-sys/src/lib.rs @@ -0,0 +1,43 @@ +//! Raw bindings to [VapourSynth](https://github.com/vapoursynth/vapoursynth). +#![doc(html_root_url = "https://docs.rs/vapoursynth-sys/0.2.2")] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +#[macro_use] +extern crate cfg_if; + +mod bindings; +pub use bindings::*; + +macro_rules! api_version { + ($major:expr, $minor:expr) => { + ($major << 16) | $minor + }; +} + +cfg_if! { + if #[cfg(feature="vapoursynth-api-35")] { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 5); + } else if #[cfg(feature="vapoursynth-api-34")] { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 4); + } else if #[cfg(feature="vapoursynth-api-33")] { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 3); + } else if #[cfg(feature="vapoursynth-api-32")] { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 2); + } else if #[cfg(feature="vapoursynth-api-31")] { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 1); + } else { + pub const VAPOURSYNTH_API_VERSION: i32 = api_version!(3, 0); + } +} + +cfg_if! { + if #[cfg(feature="vsscript-api-32")] { + pub const VSSCRIPT_API_VERSION: i32 = api_version!(3, 2); + } else if #[cfg(feature="vsscript-api-31")] { + pub const VSSCRIPT_API_VERSION: i32 = api_version!(3, 1); + } else { + pub const VSSCRIPT_API_VERSION: i32 = api_version!(3, 0); + } +} diff --git a/vapoursynth/CHANGELOG.md b/vapoursynth/CHANGELOG.md new file mode 100644 index 0000000..fb06988 --- /dev/null +++ b/vapoursynth/CHANGELOG.md @@ -0,0 +1,24 @@ +## v0.2 (16th Jun 2018) +- Added plugin support! That includes: + - `plugins::{Metadata,Filter,FilterFunction}` types and traits for making plugins; + - `export_vapoursynth_plugin!` macro for exporting a VapourSynth plugin; + - `make_filter_function!` macro for making filters without much boilerplate. +- Added a sample plugin in the `sample-filter` folder. +- Added the `component::Component` trait and `Frame::plane*()` accessors for safely working with the pixel data without having to manually transmute slices, including an optional half-precision float support using the `half` crate. +- Added `plugin::Plugin` and other relevant APIs for enumerating plugins and invoking their functions. +- Added lifetime parameters to many types to fix soundness issues. +- Split `Frame` into `Frame`, `FrameRef`, `FrameRefMut`. +- Added the `map::Value` trait and generic `Map::{get,get_iter,set,append}()` functions. +- Added format caching in `Frame` to reduce the number of API calls needed. +- Added some convenience `From` impls. + +### v0.1.2 (2nd Apr 2018) +- Fixed `Frame::data_row()` returning slices of incorrect rows (using the `plane` value instead of the `row` value). + +### v0.1.1 (24th Mar 2018) +- Added support for targetting 32-bit Windows +- Added automatic detection of common Windows VapourSynth library dirs +- Fixed `Frame::data()` and `Frame::data_row()` returning slices of incorrect sizes (too short) for pixel formats with more than 1 byte per pixel + +## v0.1.0 +- Initial release diff --git a/vapoursynth/Cargo.toml b/vapoursynth/Cargo.toml new file mode 100644 index 0000000..53abbe7 --- /dev/null +++ b/vapoursynth/Cargo.toml @@ -0,0 +1,101 @@ +[package] +name = "vapoursynth" +version = "0.2.0" # remember to update html_root_url +authors = ["Ivan Molodetskikh "] +description = "Safe Rust wrapper for VapourSynth and VSScript." +license = "MIT/Apache-2.0" + +readme = "README.md" +documentation = "https://docs.rs/vapoursynth" +repository = "https://github.com/YaLTeR/vapoursynth-rs" +keywords = ["vapoursynth", "vsscript", "video", "bindings"] +categories = ["api-bindings", "external-ffi-bindings", "multimedia::video"] + +[badges] +travis-ci = { repository = "YaLTeR/vapoursynth-rs" } +appveyor = { repository = "YaLTeR/vapoursynth-rs" } + +[dependencies] +bitflags = "1" +failure = "0.1" +failure_derive = "0.1" +half = { version = "1", optional = true } +lazy_static = "1" +vapoursynth-sys = { version = "0.2", path = "../vapoursynth-sys" } + +[dev-dependencies] +clap = "2" +lazy_static = "1" +num-rational = { version = "0.1", default-features = false } + +[features] +# Enable the half::f16 type to be used for frame pixel data. +f16-pixel-type = ["half"] + +# Features for enabling higher API versions. +vapoursynth-api-31 = [ + "vapoursynth-sys/vapoursynth-api-31", + "gte-vapoursynth-api-31" +] +vapoursynth-api-32 = [ + "vapoursynth-sys/vapoursynth-api-32", + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32" +] +vapoursynth-api-33 = [ + "vapoursynth-sys/vapoursynth-api-33", + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32", + "gte-vapoursynth-api-33" +] +vapoursynth-api-34 = [ + "vapoursynth-sys/vapoursynth-api-34", + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32", + "gte-vapoursynth-api-33", + "gte-vapoursynth-api-34" +] +vapoursynth-api-35 = [ + "vapoursynth-sys/vapoursynth-api-35", + "gte-vapoursynth-api-31", + "gte-vapoursynth-api-32", + "gte-vapoursynth-api-33", + "gte-vapoursynth-api-34", + "gte-vapoursynth-api-35" +] + +vsscript-api-31 = [ + "vapoursynth-sys/vsscript-api-31", + "gte-vsscript-api-31" +] +vsscript-api-32 = [ + "vapoursynth-sys/vsscript-api-32", + "gte-vsscript-api-31", + "gte-vsscript-api-32" +] + +# Features for linking to certain functions. +vapoursynth-functions = ["vapoursynth-sys/vapoursynth-functions"] +vsscript-functions = ["vapoursynth-sys/vsscript-functions"] + +# Utility features, not for outside use. +gte-vapoursynth-api-31 = [] +gte-vapoursynth-api-32 = [] +gte-vapoursynth-api-33 = [] +gte-vapoursynth-api-34 = [] +gte-vapoursynth-api-35 = [] + +gte-vsscript-api-31 = [] +gte-vsscript-api-32 = [] + +# For development. +#default = ["vapoursynth-api-35", "vsscript-api-32", "vapoursynth-functions", "vsscript-functions"] + +[package.metadata.docs.rs] +features = [ + "vapoursynth-api-35", + "vsscript-api-32", + "vapoursynth-functions", + "vsscript-functions", + "f16-pixel-type" +] diff --git a/vapoursynth/LICENSE-APACHE b/vapoursynth/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/vapoursynth/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/vapoursynth/LICENSE-MIT b/vapoursynth/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/vapoursynth/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/vapoursynth/README.md b/vapoursynth/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/vapoursynth/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/vapoursynth/examples/vpy-info.rs b/vapoursynth/examples/vpy-info.rs new file mode 100644 index 0000000..314d13f --- /dev/null +++ b/vapoursynth/examples/vpy-info.rs @@ -0,0 +1,202 @@ +#![allow(unused)] +#[macro_use] +extern crate failure; +extern crate vapoursynth; + +use failure::{err_msg, Error, ResultExt}; +use std::env; +use vapoursynth::prelude::*; + +fn usage() { + println!( + "Usage:\n\t{} [frame number]", + env::current_exe() + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) + .unwrap_or_else(|| "vpy-info".to_owned()) + ); +} + +#[cfg(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +))] +fn print_node_info(node: &Node) { + use std::fmt::Debug; + + // Helper function for printing properties. + fn map_or_variable(x: &Property, f: F) -> String + where + T: Debug + Clone + Copy + Eq + PartialEq, + F: FnOnce(&T) -> String, + { + match *x { + Property::Variable => "variable".to_owned(), + Property::Constant(ref x) => f(x), + } + } + + let info = node.info(); + + println!( + "Format: {}", + map_or_variable(&info.format, |x| x.name().to_owned()) + ); + println!( + "Resolution: {}", + map_or_variable(&info.resolution, |x| format!("{}×{}", x.width, x.height)) + ); + println!( + "Framerate: {}", + map_or_variable(&info.framerate, |x| format!( + "{}/{} ({})", + x.numerator, + x.denominator, + x.numerator as f64 / x.denominator as f64 + )) + ); + + #[cfg(feature = "gte-vapoursynth-api-32")] + println!("Frame count: {}", info.num_frames); + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + println!( + "Frame count: {}", + map_or_variable(&info.num_frames, |x| format!("{}", x)) + ); +} + +#[cfg(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +))] +fn run() -> Result<(), Error> { + let filename = env::args() + .nth(1) + .ok_or_else(|| err_msg("The filename argument is missing"))?; + let environment = + vsscript::Environment::from_file(filename, vsscript::EvalFlags::SetWorkingDir) + .context("Couldn't create the VSScript environment")?; + + let core = environment + .get_core() + .context("Couldn't get the VapourSynth core")?; + println!("{}", core.info()); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = environment + .get_output(0) + .context("Couldn't get the output at index 0")?; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = ( + environment + .get_output(0) + .context("Couldn't get the output at index 0")?, + None::, + ); + + print_node_info(&node); + + println!(); + if let Some(alpha_node) = alpha_node { + println!("Alpha:"); + print_node_info(&alpha_node); + } else { + println!("Alpha: No"); + } + + if let Some(n) = env::args().nth(2) { + let n = n + .parse::() + .context("Couldn't parse the frame number")?; + if n > i32::max_value() as usize { + bail!("Frame number is too big"); + } + + let frame = node.get_frame(n).context("Couldn't get the frame")?; + + println!(); + println!("Frame #{}", n); + + let format = frame.format(); + println!("Format: {}", format.name()); + println!("Plane count: {}", format.plane_count()); + + let props = frame.props(); + let count = props.key_count(); + + if count > 0 { + println!(); + } + + for k in 0..count { + let key = props.key(k); + + macro_rules! print_value { + ($func:ident) => { + println!( + "Property: {} => {:?}", + key, + props.$func(key).unwrap().collect::>() + ) + }; + } + + match props.value_type(key).unwrap() { + ValueType::Int => print_value!(get_int_iter), + ValueType::Float => print_value!(get_float_iter), + ValueType::Data => print_value!(get_data_iter), + ValueType::Node => print_value!(get_node_iter), + ValueType::Frame => print_value!(get_frame_iter), + ValueType::Function => print_value!(get_function_iter), + } + } + + for plane in 0..format.plane_count() { + println!(); + println!("Plane #{}", plane); + println!( + "Resolution: {}×{}", + frame.width(plane), + frame.height(plane) + ); + println!("Stride: {}", frame.stride(plane)); + } + } + + Ok(()) +} + +#[cfg(not(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +)))] +fn run() -> Result<(), Error> { + bail!( + "This example requires the `vsscript-functions` and either `vapoursynth-functions` or \ + `vsscript-api-32` features." + ) +} + +fn main() { + if let Err(err) = run() { + eprintln!("Error: {}", err.cause()); + + for cause in err.causes().skip(1) { + eprintln!("Caused by: {}", cause); + } + + eprintln!("{}", err.backtrace()); + + usage(); + } +} diff --git a/vapoursynth/examples/vspipe.rs b/vapoursynth/examples/vspipe.rs new file mode 100644 index 0000000..0bbe805 --- /dev/null +++ b/vapoursynth/examples/vspipe.rs @@ -0,0 +1,893 @@ +// This is a mostly drop-in reimplementation of vspipe. +// The main difference is what the errors look like. +#![allow(unused)] +#[macro_use] +extern crate failure; + +use failure::{err_msg, Error, ResultExt}; + +#[cfg(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +))] +mod inner { + #![cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] + #![cfg_attr(feature = "cargo-clippy", allow(mutex_atomic))] + extern crate clap; + extern crate num_rational; + extern crate vapoursynth; + + use std::cmp; + use std::collections::HashMap; + use std::ffi::OsStr; + use std::fmt::Debug; + use std::fs::File; + use std::io::{self, stdout, Stdout, Write}; + use std::ops::Deref; + use std::sync::{Arc, Condvar, Mutex}; + use std::time::Instant; + + use self::clap::{App, Arg}; + use self::num_rational::Ratio; + use self::vapoursynth::prelude::*; + use super::*; + + enum OutputTarget { + File(File), + Stdout(Stdout), + Empty, + } + + struct OutputParameters<'core> { + node: Node<'core>, + alpha_node: Option>, + start_frame: usize, + end_frame: usize, + requests: usize, + y4m: bool, + progress: bool, + } + + struct OutputState<'core> { + output_target: OutputTarget, + timecodes_file: Option, + error: Option<(usize, Error)>, + reorder_map: HashMap>, Option>)>, + last_requested_frame: usize, + next_output_frame: usize, + current_timecode: Ratio, + callbacks_fired: usize, + callbacks_fired_alpha: usize, + last_fps_report_time: Instant, + last_fps_report_frames: usize, + fps: Option, + } + + struct SharedData<'core> { + output_done_pair: (Mutex, Condvar), + output_parameters: OutputParameters<'core>, + output_state: Mutex>, + } + + impl Write for OutputTarget { + fn write(&mut self, buf: &[u8]) -> io::Result { + match *self { + OutputTarget::File(ref mut file) => file.write(buf), + OutputTarget::Stdout(ref mut out) => out.write(buf), + OutputTarget::Empty => Ok(buf.len()), + } + } + + fn flush(&mut self) -> io::Result<()> { + match *self { + OutputTarget::File(ref mut file) => file.flush(), + OutputTarget::Stdout(ref mut out) => out.flush(), + OutputTarget::Empty => Ok(()), + } + } + } + + fn print_version() -> Result<(), Error> { + let environment = Environment::new().context("Couldn't create the VSScript environment")?; + let core = environment + .get_core() + .context("Couldn't create the VapourSynth core")?; + print!("{}", core.info().version_string); + + Ok(()) + } + + // Parses the --arg arguments in form of key=value. + fn parse_arg(arg: &str) -> Result<(&str, &str), Error> { + arg.find('=') + .map(|index| arg.split_at(index)) + .map(|(k, v)| (k, &v[1..])) + .ok_or_else(|| format_err!("No value specified for argument: {}", arg)) + } + + // Returns "Variable" or the value of the property passed through a function. + fn map_or_variable(x: &Property, f: F) -> String + where + T: Debug + Clone + Copy + Eq + PartialEq, + F: FnOnce(&T) -> String, + { + match *x { + Property::Variable => "Variable".to_owned(), + Property::Constant(ref x) => f(x), + } + } + + fn print_info(writer: &mut Write, node: &Node, alpha: Option<&Node>) -> Result<(), Error> { + let info = node.info(); + + writeln!( + writer, + "Width: {}", + map_or_variable(&info.resolution, |x| format!("{}", x.width)) + )?; + writeln!( + writer, + "Height: {}", + map_or_variable(&info.resolution, |x| format!("{}", x.height)) + )?; + + #[cfg(feature = "gte-vapoursynth-api-32")] + writeln!(writer, "Frames: {}", info.num_frames)?; + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + writeln!( + writer, + "Frames: {}", + match info.num_frames { + Property::Variable => "Unknown".to_owned(), + Property::Constant(x) => format!("{}", x), + } + )?; + + writeln!( + writer, + "FPS: {}", + map_or_variable(&info.framerate, |x| format!( + "{}/{} ({:.3} fps)", + x.numerator, + x.denominator, + x.numerator as f64 / x.denominator as f64 + )) + )?; + + match info.format { + Property::Variable => writeln!(writer, "Format Name: Variable")?, + Property::Constant(f) => { + writeln!(writer, "Format Name: {}", f.name())?; + writeln!(writer, "Color Family: {}", f.color_family())?; + writeln!( + writer, + "Alpha: {}", + if alpha.is_some() { "Yes" } else { "No" } + )?; + writeln!(writer, "Sample Type: {}", f.sample_type())?; + writeln!(writer, "Bits: {}", f.bits_per_sample())?; + writeln!(writer, "SubSampling W: {}", f.sub_sampling_w())?; + writeln!(writer, "SubSampling H: {}", f.sub_sampling_h())?; + } + } + + Ok(()) + } + + fn print_y4m_header(writer: &mut W, node: &Node) -> Result<(), Error> { + let info = node.info(); + + if let Property::Constant(format) = info.format { + write!(writer, "YUV4MPEG2 C")?; + + match format.color_family() { + ColorFamily::Gray => { + write!(writer, "mono")?; + if format.bits_per_sample() > 8 { + write!(writer, "{}", format.bits_per_sample())?; + } + } + ColorFamily::YUV => { + write!( + writer, + "{}", + match (format.sub_sampling_w(), format.sub_sampling_h()) { + (1, 1) => "420", + (1, 0) => "422", + (0, 0) => "444", + (2, 2) => "410", + (2, 0) => "411", + (0, 1) => "440", + _ => bail!("No y4m identifier exists for the current format"), + } + )?; + + if format.bits_per_sample() > 8 && format.sample_type() == SampleType::Integer { + write!(writer, "p{}", format.bits_per_sample())?; + } else if format.sample_type() == SampleType::Float { + write!( + writer, + "p{}", + match format.bits_per_sample() { + 16 => "h", + 32 => "s", + 64 => "d", + _ => unreachable!(), + } + )?; + } + } + _ => bail!("No y4m identifier exists for the current format"), + } + + if let Property::Constant(resolution) = info.resolution { + write!(writer, " W{} H{}", resolution.width, resolution.height)?; + } else { + unreachable!(); + } + + if let Property::Constant(framerate) = info.framerate { + write!( + writer, + " F{}:{}", + framerate.numerator, framerate.denominator + )?; + } else { + unreachable!(); + } + + #[cfg(feature = "gte-vapoursynth-api-32")] + let num_frames = info.num_frames; + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + let num_frames = { + if let Property::Constant(num_frames) = info.num_frames { + num_frames + } else { + unreachable!(); + } + }; + + write!(writer, " Ip A0:0 XLENGTH={}\n", num_frames)?; + + Ok(()) + } else { + unreachable!(); + } + } + + // Checks if the frame is completed, that is, we have the frame and, if needed, its alpha part. + fn is_completed(entry: &(Option, Option), have_alpha: bool) -> bool { + entry.0.is_some() && (!have_alpha || entry.1.is_some()) + } + + fn print_frame(writer: &mut W, frame: &Frame) -> Result<(), Error> { + const RGB_REMAP: [usize; 3] = [1, 2, 0]; + + let format = frame.format(); + #[cfg_attr(feature = "cargo-clippy", allow(needless_range_loop))] + for plane in 0..format.plane_count() { + let plane = if format.color_family() == ColorFamily::RGB { + RGB_REMAP[plane] + } else { + plane + }; + + if let Ok(data) = frame.data(plane) { + writer.write_all(data)?; + } else { + for row in 0..frame.height(plane) { + writer.write_all(frame.data_row(plane, row))?; + } + } + } + + Ok(()) + } + + fn print_frames( + writer: &mut W, + parameters: &OutputParameters, + frame: &Frame, + alpha_frame: Option<&Frame>, + ) -> Result<(), Error> { + if parameters.y4m { + write!(writer, "FRAME\n").context("Couldn't output the frame header")?; + } + + print_frame(writer, frame).context("Couldn't output the frame")?; + if let Some(alpha_frame) = alpha_frame { + print_frame(writer, alpha_frame).context("Couldn't output the alpha frame")?; + } + + Ok(()) + } + + fn update_timecodes(frame: &Frame, state: &mut OutputState) -> Result<(), Error> { + let props = frame.props(); + let duration_num = props + .get_int("_DurationNum") + .context("Couldn't get the duration numerator")?; + let duration_den = props + .get_int("_DurationDen") + .context("Couldn't get the duration denominator")?; + + if duration_den == 0 { + bail!("The duration denominator is zero"); + } + + state.current_timecode += Ratio::new(duration_num, duration_den); + + Ok(()) + } + + fn frame_done_callback<'core>( + frame: Result, GetFrameError>, + n: usize, + _node: &Node<'core>, + shared_data: &Arc>, + alpha: bool, + ) { + let parameters = &shared_data.output_parameters; + let mut state = shared_data.output_state.lock().unwrap(); + + // Increase the progress counter. + if !alpha { + state.callbacks_fired += 1; + if parameters.alpha_node.is_none() { + state.callbacks_fired_alpha += 1; + } + } else { + state.callbacks_fired_alpha += 1; + } + + // Figure out the FPS. + if parameters.progress { + let current = Instant::now(); + let elapsed = current.duration_since(state.last_fps_report_time); + let elapsed_seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; + + if elapsed.as_secs() > 10 { + state.fps = Some( + (state.callbacks_fired - state.last_fps_report_frames) as f64 / elapsed_seconds, + ); + state.last_fps_report_time = current; + state.last_fps_report_frames = state.callbacks_fired; + } + } + + match frame { + Err(error) => { + if state.error.is_none() { + state.error = Some(( + n, + err_msg(error.into_inner().to_string_lossy().into_owned()), + )) + } + } + Ok(frame) => { + // Store the frame in the reorder map. + { + let entry = state.reorder_map.entry(n).or_insert((None, None)); + if alpha { + entry.1 = Some(frame); + } else { + entry.0 = Some(frame); + } + } + + // If we got both a frame and its alpha frame, request one more. + if is_completed(&state.reorder_map[&n], parameters.alpha_node.is_some()) + && state.last_requested_frame < parameters.end_frame + && state.error.is_none() + { + let shared_data_2 = shared_data.clone(); + parameters.node.get_frame_async( + state.last_requested_frame + 1, + move |frame, n, node| { + frame_done_callback(frame, n, &node, &shared_data_2, false) + }, + ); + + if let Some(ref alpha_node) = parameters.alpha_node { + let shared_data_2 = shared_data.clone(); + alpha_node.get_frame_async( + state.last_requested_frame + 1, + move |frame, n, node| { + frame_done_callback(frame, n, &node, &shared_data_2, true) + }, + ); + } + + state.last_requested_frame += 1; + } + + // Output all completed frames. + while state + .reorder_map + .get(&state.next_output_frame) + .map(|entry| is_completed(entry, parameters.alpha_node.is_some())) + .unwrap_or(false) + { + let next_output_frame = state.next_output_frame; + let (frame, alpha_frame) = + state.reorder_map.remove(&next_output_frame).unwrap(); + + let frame = frame.unwrap(); + if state.error.is_none() { + if let Err(error) = print_frames( + &mut state.output_target, + parameters, + &frame, + alpha_frame.as_ref().map(Deref::deref), + ) { + state.error = Some((n, error)); + } + } + + if state.timecodes_file.is_some() && state.error.is_none() { + let timecode = (*state.current_timecode.numer() as f64 * 1000f64) + / *state.current_timecode.denom() as f64; + match writeln!(state.timecodes_file.as_mut().unwrap(), "{:.6}", timecode) + .context("Couldn't output the timecode") + { + Err(error) => state.error = Some((n, error.into())), + Ok(()) => if let Err(error) = update_timecodes(&frame, &mut state) + .context("Couldn't update the timecodes") + { + state.error = Some((n, error.into())); + }, + } + } + + state.next_output_frame += 1; + } + } + } + + // Output the progress info. + if parameters.progress { + eprint!( + "Frame: {}/{}", + state.callbacks_fired, + parameters.end_frame - parameters.start_frame + 1 + ); + + if let Some(fps) = state.fps { + eprint!(" ({:.2} fps)", fps); + } + + eprint!("\r"); + } + + // if state.next_output_frame == parameters.end_frame + 1 { + // This condition works with error handling: + let frames_requested = state.last_requested_frame - parameters.start_frame + 1; + if state.callbacks_fired == frames_requested + && state.callbacks_fired_alpha == frames_requested + { + *shared_data.output_done_pair.0.lock().unwrap() = true; + shared_data.output_done_pair.1.notify_one(); + } + } + + fn output( + mut output_target: OutputTarget, + mut timecodes_file: Option, + parameters: OutputParameters, + ) -> Result<(), Error> { + // Print the y4m header. + if parameters.y4m { + if parameters.alpha_node.is_some() { + bail!("Can't apply y4m headers to a clip with alpha"); + } + + print_y4m_header(&mut output_target, ¶meters.node) + .context("Couldn't write the y4m header")?; + } + + // Print the timecodes header. + if let Some(ref mut timecodes_file) = timecodes_file { + writeln!(timecodes_file, "# timecode format v2")?; + } + + let initial_requests = cmp::min( + parameters.requests, + parameters.end_frame - parameters.start_frame + 1, + ); + + let output_done_pair = (Mutex::new(false), Condvar::new()); + let output_state = Mutex::new(OutputState { + output_target, + timecodes_file, + error: None, + reorder_map: HashMap::new(), + last_requested_frame: parameters.start_frame + initial_requests - 1, + next_output_frame: 0, + current_timecode: Ratio::from_integer(0), + callbacks_fired: 0, + callbacks_fired_alpha: 0, + last_fps_report_time: Instant::now(), + last_fps_report_frames: 0, + fps: None, + }); + let shared_data = Arc::new(SharedData { + output_done_pair, + output_parameters: parameters, + output_state, + }); + + // Record the start time. + let start_time = Instant::now(); + + // Start off by requesting some frames. + { + let parameters = &shared_data.output_parameters; + for n in 0..initial_requests { + let shared_data_2 = shared_data.clone(); + parameters.node.get_frame_async(n, move |frame, n, node| { + frame_done_callback(frame, n, &node, &shared_data_2, false) + }); + + if let Some(ref alpha_node) = parameters.alpha_node { + let shared_data_2 = shared_data.clone(); + alpha_node.get_frame_async(n, move |frame, n, node| { + frame_done_callback(frame, n, &node, &shared_data_2, true) + }); + } + } + } + + let &(ref lock, ref cvar) = &shared_data.output_done_pair; + let mut done = lock.lock().unwrap(); + while !*done { + done = cvar.wait(done).unwrap(); + } + + let elapsed = start_time.elapsed(); + let elapsed_seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; + + let mut state = shared_data.output_state.lock().unwrap(); + eprintln!( + "Output {} frames in {:.2} seconds ({:.2} fps)", + state.next_output_frame, + elapsed_seconds, + state.next_output_frame as f64 / elapsed_seconds + ); + + if let Some((n, ref msg)) = state.error { + bail!("Failed to retrieve frame {} with error: {}", n, msg); + } + + // Flush the output file. + state + .output_target + .flush() + .context("Failed to flush the output file")?; + + Ok(()) + } + + pub fn run() -> Result<(), Error> { + let matches = App::new("vspipe-rs") + .about("A Rust implementation of vspipe") + .author("Ivan M. ") + .arg( + Arg::with_name("arg") + .short("a") + .long("arg") + .takes_value(true) + .multiple(true) + .number_of_values(1) + .value_name("key=value") + .display_order(1) + .help("Argument to pass to the script environment") + .long_help( + "Argument to pass to the script environment, \ + a key with this name and value (bytes typed) \ + will be set in the globals dict", + ), + ).arg( + Arg::with_name("start") + .short("s") + .long("start") + .takes_value(true) + .value_name("N") + .display_order(2) + .help("First frame to output"), + ).arg( + Arg::with_name("end") + .short("e") + .long("end") + .takes_value(true) + .value_name("N") + .display_order(3) + .help("Last frame to output"), + ).arg( + Arg::with_name("outputindex") + .short("o") + .long("outputindex") + .takes_value(true) + .value_name("N") + .display_order(4) + .help("Output index"), + ).arg( + Arg::with_name("requests") + .short("r") + .long("requests") + .takes_value(true) + .value_name("N") + .display_order(5) + .help("Number of concurrent frame requests"), + ).arg( + Arg::with_name("y4m") + .short("y") + .long("y4m") + .help("Add YUV4MPEG headers to output"), + ).arg( + Arg::with_name("timecodes") + .short("t") + .long("timecodes") + .takes_value(true) + .value_name("FILE") + .display_order(6) + .help("Write timecodes v2 file"), + ).arg( + Arg::with_name("progress") + .short("p") + .long("progress") + .help("Print progress to stderr"), + ).arg( + Arg::with_name("info") + .short("i") + .long("info") + .help("Show video info and exit"), + ).arg( + Arg::with_name("version") + .short("v") + .long("version") + .help("Show version info and exit") + .conflicts_with_all(&[ + "info", + "progress", + "y4m", + "arg", + "start", + "end", + "outputindex", + "requests", + "timecodes", + "script", + "outfile", + ]), + ).arg( + Arg::with_name("script") + .required_unless("version") + .index(1) + .help("Input .vpy file"), + ).arg( + Arg::with_name("outfile") + .required_unless("version") + .index(2) + .help("Output file") + .long_help( + "Output file, use hyphen `-` for stdout \ + or dot `.` for suppressing any output", + ), + ).get_matches(); + + // Check --version. + if matches.is_present("version") { + return print_version(); + } + + // Open the output files. + let mut output_target = match matches.value_of_os("outfile").unwrap() { + x if x == OsStr::new(".") => OutputTarget::Empty, + x if x == OsStr::new("-") => OutputTarget::Stdout(stdout()), + path => { + OutputTarget::File(File::create(path).context("Couldn't open the output file")?) + } + }; + + let timecodes_file = match matches.value_of_os("timecodes") { + Some(path) => { + Some(File::create(path).context("Couldn't open the timecodes output file")?) + } + None => None, + }; + + // Create a new VSScript environment. + let mut environment = + Environment::new().context("Couldn't create the VSScript environment")?; + + // Parse and set the --arg arguments. + if let Some(args) = matches.values_of("arg") { + let mut args_map = OwnedMap::new(API::get().unwrap()); + + for arg in args.map(parse_arg) { + let (name, value) = arg.context("Couldn't parse an argument")?; + args_map + .append_data(name, value.as_bytes()) + .context("Couldn't append an argument value")?; + } + + environment + .set_variables(&args_map) + .context("Couldn't set arguments")?; + } + + // Start time more similar to vspipe's. + let start_time = Instant::now(); + + // Evaluate the script. + environment + .eval_file( + matches.value_of("script").unwrap(), + EvalFlags::SetWorkingDir, + ).context("Script evaluation failed")?; + + // Get the output node. + let output_index = matches + .value_of("outputindex") + .map(str::parse) + .unwrap_or(Ok(0)) + .context("Couldn't convert the output index to an integer")?; + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = environment.get_output(output_index).context(format!( + "Couldn't get the output node at index {}", + output_index + ))?; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = ( + environment.get_output(output_index).context(format!( + "Couldn't get the output node at index {}", + output_index + ))?, + None::, + ); + + if matches.is_present("info") { + print_info(&mut output_target, &node, alpha_node.as_ref()) + .context("Couldn't print info to the output file")?; + + output_target + .flush() + .context("Couldn't flush the output file")?; + } else { + let num_frames = { + let info = node.info(); + + if let Property::Variable = info.format { + bail!("Cannot output clips with varying format"); + } + if let Property::Variable = info.resolution { + bail!("Cannot output clips with varying dimensions"); + } + if let Property::Variable = info.framerate { + bail!("Cannot output clips with varying framerate"); + } + + #[cfg(feature = "gte-vapoursynth-api-32")] + let num_frames = info.num_frames; + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + let num_frames = { + match info.num_frames { + Property::Variable => { + // TODO: make it possible? + bail!("Cannot output clips with unknown length"); + } + Property::Constant(x) => x, + } + }; + + num_frames + }; + + let start_frame = matches + .value_of("start") + .map(str::parse::) + .unwrap_or(Ok(0)) + .context("Couldn't convert the start frame to an integer")?; + let end_frame = matches + .value_of("end") + .map(str::parse::) + .unwrap_or_else(|| Ok(num_frames as i32 - 1)) + .context("Couldn't convert the end frame to an integer")?; + + // Check if the input start and end frames make sense. + if start_frame < 0 || end_frame < start_frame || end_frame as usize >= num_frames { + bail!( + "Invalid range of frames to output specified:\n\ + first: {}\n\ + last: {}\n\ + clip length: {}\n\ + frames to output: {}", + start_frame, + end_frame, + num_frames, + end_frame + .checked_sub(start_frame) + .and_then(|x| x.checked_add(1)) + .map(|x| format!("{}", x)) + .unwrap_or_else(|| "".to_owned()) + ); + } + + let requests = { + let requests = matches + .value_of("requests") + .map(str::parse::) + .unwrap_or(Ok(0)) + .context("Couldn't convert the request count to an unsigned integer")?; + + if requests == 0 { + environment.get_core().unwrap().info().num_threads + } else { + requests + } + }; + + let y4m = matches.is_present("y4m"); + let progress = matches.is_present("progress"); + + output( + output_target, + timecodes_file, + OutputParameters { + node, + alpha_node, + start_frame: start_frame as usize, + end_frame: end_frame as usize, + requests, + y4m, + progress, + }, + ).context("Couldn't output the frames")?; + + // This is still not a very valid comparison since vspipe does all argument validation + // before it starts the time. + let elapsed = start_time.elapsed(); + let elapsed_seconds = elapsed.as_secs() as f64 + elapsed.subsec_nanos() as f64 * 1e-9; + eprintln!("vspipe time: {:.2} seconds", elapsed_seconds); + } + + Ok(()) + } +} + +#[cfg(not(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +)))] +mod inner { + use super::*; + + pub fn run() -> Result<(), Error> { + bail!( + "This example requires the `vsscript-functions` and either `vapoursynth-functions` or \ + `vsscript-api-32` features." + ) + } +} + +fn main() { + if let Err(err) = inner::run() { + eprintln!("Error: {}", err.cause()); + + for cause in err.causes().skip(1) { + eprintln!("Caused by: {}", cause); + } + + eprintln!("{}", err.backtrace()); + } +} diff --git a/vapoursynth/src/api.rs b/vapoursynth/src/api.rs new file mode 100644 index 0000000..762fb5f --- /dev/null +++ b/vapoursynth/src/api.rs @@ -0,0 +1,1003 @@ +//! Most general VapourSynth API functions. + +use std::ffi::{CStr, CString, NulError}; +use std::os::raw::{c_char, c_int, c_void}; +use std::ptr::{self, NonNull}; +use std::sync::atomic::{AtomicPtr, Ordering}; +use std::{mem, panic, process}; +use vapoursynth_sys as ffi; + +/// A wrapper for the VapourSynth API. +#[derive(Debug, Clone, Copy)] +pub struct API { + // Note that this is *const, not *mut. + handle: NonNull, +} + +unsafe impl Send for API {} +unsafe impl Sync for API {} + +/// A cached API pointer. Note that this is `*const ffi::VSAPI`, not `*mut`. +static RAW_API: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + +/// VapourSynth log message types. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum MessageType { + Debug, + Warning, + Critical, + + /// The process will `abort()` after the message handler returns. + Fatal, +} + +// Macros for implementing repetitive functions. +macro_rules! prop_get_something { + ($name:ident, $func:ident, $rv:ty) => ( + #[inline] + pub(crate) unsafe fn $name( + self, + map: &ffi::VSMap, + key: *const c_char, + index: i32, + error: &mut i32, + ) -> $rv { + (self.handle.as_ref().$func)(map, key, index, error) + } + ) +} + +macro_rules! prop_set_something { + ($name:ident, $func:ident, $type:ty) => ( + #[inline] + pub(crate) unsafe fn $name( + self, + map: &mut ffi::VSMap, + key: *const c_char, + value: $type, + append: ffi::VSPropAppendMode, + ) -> i32 { + (self.handle.as_ref().$func)(map, key, value, append as i32) + } + ) +} + +impl API { + /// Retrieves the VapourSynth API. + /// + /// Returns `None` on error, for example if the requested API version (selected with features, + /// see the crate-level docs) is not supported. + // If we're linking to VSScript anyway, use the VSScript function. + #[cfg(all( + feature = "vsscript-functions", + feature = "gte-vsscript-api-32" + ))] + #[inline] + pub fn get() -> Option { + use vsscript; + + // Check if we already have the API. + let handle = RAW_API.load(Ordering::Relaxed); + + let handle = if handle.is_null() { + // Attempt retrieving it otherwise. + vsscript::maybe_initialize(); + let handle = + unsafe { ffi::vsscript_getVSApi2(ffi::VAPOURSYNTH_API_VERSION) } as *mut ffi::VSAPI; + + if !handle.is_null() { + // If we successfully retrieved the API, cache it. + RAW_API.store(handle, Ordering::Relaxed); + } + handle + } else { + handle + }; + + if handle.is_null() { + None + } else { + Some(Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + }) + } + } + + /// Retrieves the VapourSynth API. + /// + /// Returns `None` on error, for example if the requested API version (selected with features, + /// see the crate-level docs) is not supported. + #[cfg(all( + feature = "vapoursynth-functions", + not(all( + feature = "vsscript-functions", + feature = "gte-vsscript-api-32" + )) + ))] + #[inline] + pub fn get() -> Option { + // Check if we already have the API. + let handle = RAW_API.load(Ordering::Relaxed); + + let handle = if handle.is_null() { + // Attempt retrieving it otherwise. + let handle = + unsafe { ffi::getVapourSynthAPI(ffi::VAPOURSYNTH_API_VERSION) } as *mut ffi::VSAPI; + + if !handle.is_null() { + // If we successfully retrieved the API, cache it. + RAW_API.store(handle, Ordering::Relaxed); + } + handle + } else { + handle + }; + + if handle.is_null() { + None + } else { + Some(Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + }) + } + } + + /// Returns the cached API. + /// + /// # Safety + /// This function assumes the cache contains a valid API pointer. + #[inline] + pub(crate) unsafe fn get_cached() -> Self { + Self { + handle: NonNull::new_unchecked(RAW_API.load(Ordering::Relaxed)), + } + } + + /// Stores the API in the cache. + /// + /// # Safety + /// The given pointer should be valid. + #[inline] + pub(crate) unsafe fn set(handle: *const ffi::VSAPI) { + RAW_API.store(handle as *mut _, Ordering::Relaxed); + } + + /// Sends a message through VapourSynth’s logging framework. + #[cfg(feature = "gte-vapoursynth-api-34")] + #[inline] + pub fn log(self, message_type: MessageType, message: &str) -> Result<(), NulError> { + let message = CString::new(message)?; + unsafe { + (self.handle.as_ref().logMessage)(message_type.ffi_type(), message.as_ptr()); + } + Ok(()) + } + + /// Installs a custom handler for the various error messages VapourSynth emits. The message + /// handler is currently global, i.e. per process, not per VSCore instance. + /// + /// The default message handler simply sends the messages to the standard error stream. + /// + /// The callback arguments are the message type and the message itself. If the callback panics, + /// the process is aborted. + /// + /// This function allocates to store the callback, this memory is leaked if the message handler + /// is subsequently changed. + #[inline] + pub fn set_message_handler(self, callback: F) + where + F: FnMut(MessageType, &CStr) + Send + 'static, + { + struct CallbackData { + callback: Box, + } + + unsafe extern "system" fn c_callback( + msg_type: c_int, + msg: *const c_char, + user_data: *mut c_void, + ) { + let mut user_data = Box::from_raw(user_data as *mut CallbackData); + + { + let closure = panic::AssertUnwindSafe(|| { + let message_type = MessageType::from_ffi_type(msg_type).unwrap(); + let message = CStr::from_ptr(msg); + + (user_data.callback)(message_type, message); + }); + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } + } + + // Don't drop user_data, we're not done using it. + mem::forget(user_data); + } + + let user_data = Box::new(CallbackData { + callback: Box::new(callback), + }); + + unsafe { + (self.handle.as_ref().setMessageHandler)( + Some(c_callback), + Box::into_raw(user_data) as *mut c_void, + ); + } + } + + /// Installs a custom handler for the various error messages VapourSynth emits. The message + /// handler is currently global, i.e. per process, not per VSCore instance. + /// + /// The default message handler simply sends the messages to the standard error stream. + /// + /// The callback arguments are the message type and the message itself. If the callback panics, + /// the process is aborted. + /// + /// This version does not allocate at the cost of accepting a function pointer rather than an + /// arbitrary closure. It can, however, be used with simple closures. + #[inline] + pub fn set_message_handler_trivial(self, callback: fn(MessageType, &CStr)) { + unsafe extern "system" fn c_callback( + msg_type: c_int, + msg: *const c_char, + user_data: *mut c_void, + ) { + let closure = panic::AssertUnwindSafe(|| { + let message_type = MessageType::from_ffi_type(msg_type).unwrap(); + let message = CStr::from_ptr(msg); + + // Is there a better way of casting this? + let callback = *(&user_data as *const _ as *const fn(MessageType, &CStr)); + (callback)(message_type, message); + }); + + if panic::catch_unwind(closure).is_err() { + eprintln!("panic in the set_message_handler_trivial() callback, aborting"); + process::abort(); + } + } + + unsafe { + (self.handle.as_ref().setMessageHandler)(Some(c_callback), callback as *mut c_void); + } + } + + /// Clears any custom message handler, restoring the default one. + #[inline] + pub fn clear_message_handler(self) { + unsafe { + (self.handle.as_ref().setMessageHandler)(None, ptr::null_mut()); + } + } + + /// Frees `node`. + /// + /// # Safety + /// The caller must ensure `node` is valid. + #[inline] + pub(crate) unsafe fn free_node(self, node: *mut ffi::VSNodeRef) { + (self.handle.as_ref().freeNode)(node); + } + + /// Clones `node`. + /// + /// # Safety + /// The caller must ensure `node` is valid. + #[inline] + pub(crate) unsafe fn clone_node(self, node: *mut ffi::VSNodeRef) -> *mut ffi::VSNodeRef { + (self.handle.as_ref().cloneNodeRef)(node) + } + + /// Returns a pointer to the video info associated with `node`. The pointer is valid as long as + /// the node lives. + /// + /// # Safety + /// The caller must ensure `node` is valid. + #[inline] + pub(crate) unsafe fn get_video_info( + self, + node: *mut ffi::VSNodeRef, + ) -> *const ffi::VSVideoInfo { + (self.handle.as_ref().getVideoInfo)(node) + } + + /// Generates a frame directly. + /// + /// # Safety + /// The caller must ensure `node` is valid. + /// + /// # Panics + /// Panics if `err_msg` is larger than `i32::max_value()`. + #[inline] + pub(crate) unsafe fn get_frame( + self, + n: i32, + node: *mut ffi::VSNodeRef, + err_msg: &mut [c_char], + ) -> *const ffi::VSFrameRef { + let len = err_msg.len(); + assert!(len <= i32::max_value() as usize); + let len = len as i32; + + (self.handle.as_ref().getFrame)(n, node, err_msg.as_mut_ptr(), len) + } + + /// Generates a frame directly. + /// + /// # Safety + /// The caller must ensure `node` and `callback` are valid. + #[inline] + pub(crate) unsafe fn get_frame_async( + self, + n: i32, + node: *mut ffi::VSNodeRef, + callback: ffi::VSFrameDoneCallback, + user_data: *mut c_void, + ) { + (self.handle.as_ref().getFrameAsync)(n, node, callback, user_data); + } + + /// Frees `frame`. + /// + /// # Safety + /// The caller must ensure `frame` is valid. + #[inline] + pub(crate) unsafe fn free_frame(self, frame: &ffi::VSFrameRef) { + (self.handle.as_ref().freeFrame)(frame); + } + + /// Clones `frame`. + /// + /// # Safety + /// The caller must ensure `frame` is valid. + #[inline] + pub(crate) unsafe fn clone_frame(self, frame: &ffi::VSFrameRef) -> *const ffi::VSFrameRef { + (self.handle.as_ref().cloneFrameRef)(frame) + } + + /// Retrieves the format of a frame. + /// + /// # Safety + /// The caller must ensure `frame` is valid. + #[inline] + pub(crate) unsafe fn get_frame_format(self, frame: &ffi::VSFrameRef) -> *const ffi::VSFormat { + (self.handle.as_ref().getFrameFormat)(frame) + } + + /// Returns the width of a plane of a given frame, in pixels. + /// + /// # Safety + /// The caller must ensure `frame` is valid and `plane` is valid for the given `frame`. + #[inline] + pub(crate) unsafe fn get_frame_width(self, frame: &ffi::VSFrameRef, plane: i32) -> i32 { + (self.handle.as_ref().getFrameWidth)(frame, plane) + } + + /// Returns the height of a plane of a given frame, in pixels. + /// + /// # Safety + /// The caller must ensure `frame` is valid and `plane` is valid for the given `frame`. + #[inline] + pub(crate) unsafe fn get_frame_height(self, frame: &ffi::VSFrameRef, plane: i32) -> i32 { + (self.handle.as_ref().getFrameHeight)(frame, plane) + } + + /// Returns the distance in bytes between two consecutive lines of a plane of a frame. + /// + /// # Safety + /// The caller must ensure `frame` is valid and `plane` is valid for the given `frame`. + #[inline] + pub(crate) unsafe fn get_frame_stride(self, frame: &ffi::VSFrameRef, plane: i32) -> i32 { + (self.handle.as_ref().getStride)(frame, plane) + } + + /// Returns a read-only pointer to a plane of a frame. + /// + /// # Safety + /// The caller must ensure `frame` is valid and `plane` is valid for the given `frame`. + #[inline] + pub(crate) unsafe fn get_frame_read_ptr( + self, + frame: &ffi::VSFrameRef, + plane: i32, + ) -> *const u8 { + (self.handle.as_ref().getReadPtr)(frame, plane) + } + + /// Returns a read-write pointer to a plane of a frame. + /// + /// # Safety + /// The caller must ensure `frame` is valid and `plane` is valid for the given `frame`. + #[inline] + pub(crate) unsafe fn get_frame_write_ptr( + self, + frame: &mut ffi::VSFrameRef, + plane: i32, + ) -> *mut u8 { + (self.handle.as_ref().getWritePtr)(frame, plane) + } + + /// Returns a read-only pointer to a frame's properties. + /// + /// # Safety + /// The caller must ensure `frame` is valid and the correct lifetime is assigned to the + /// returned map (it can't outlive `frame`). + #[inline] + pub(crate) unsafe fn get_frame_props_ro(self, frame: &ffi::VSFrameRef) -> *const ffi::VSMap { + (self.handle.as_ref().getFramePropsRO)(frame) + } + + /// Returns a read-write pointer to a frame's properties. + /// + /// # Safety + /// The caller must ensure `frame` is valid and the correct lifetime is assigned to the + /// returned map (it can't outlive `frame`). + #[inline] + pub(crate) unsafe fn get_frame_props_rw(self, frame: &mut ffi::VSFrameRef) -> *mut ffi::VSMap { + (self.handle.as_ref().getFramePropsRW)(frame) + } + + /// Creates a new `VSMap`. + #[inline] + pub(crate) fn create_map(self) -> *mut ffi::VSMap { + unsafe { (self.handle.as_ref().createMap)() } + } + + /// Clears `map`. + /// + /// # Safety + /// The caller must ensure `map` is valid. + #[inline] + pub(crate) unsafe fn clear_map(self, map: &mut ffi::VSMap) { + (self.handle.as_ref().clearMap)(map); + } + + /// Frees `map`. + /// + /// # Safety + /// The caller must ensure `map` is valid. + #[inline] + pub(crate) unsafe fn free_map(self, map: &mut ffi::VSMap) { + (self.handle.as_ref().freeMap)(map); + } + + /// Returns a pointer to the error message contained in the map, or NULL if there is no error + /// message. + /// + /// # Safety + /// The caller must ensure `map` is valid. + #[inline] + pub(crate) unsafe fn get_error(self, map: &ffi::VSMap) -> *const c_char { + (self.handle.as_ref().getError)(map) + } + + /// Adds an error message to a map. The map is cleared first. The error message is copied. + /// + /// # Safety + /// The caller must ensure `map` and `errorMessage` are valid. + #[inline] + pub(crate) unsafe fn set_error(self, map: &mut ffi::VSMap, error_message: *const c_char) { + (self.handle.as_ref().setError)(map, error_message) + } + + /// Returns the number of keys contained in a map. + /// + /// # Safety + /// The caller must ensure `map` is valid. + #[inline] + pub(crate) unsafe fn prop_num_keys(self, map: &ffi::VSMap) -> i32 { + (self.handle.as_ref().propNumKeys)(map) + } + + /// Returns a key from a property map. + /// + /// # Safety + /// The caller must ensure `map` is valid and `index` is valid for `map`. + #[inline] + pub(crate) unsafe fn prop_get_key(self, map: &ffi::VSMap, index: i32) -> *const c_char { + (self.handle.as_ref().propGetKey)(map, index) + } + + /// Removes the key from a property map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[inline] + pub(crate) unsafe fn prop_delete_key(self, map: &mut ffi::VSMap, key: *const c_char) -> i32 { + (self.handle.as_ref().propDeleteKey)(map, key) + } + + /// Returns the number of elements associated with a key in a property map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[inline] + pub(crate) unsafe fn prop_num_elements(self, map: &ffi::VSMap, key: *const c_char) -> i32 { + (self.handle.as_ref().propNumElements)(map, key) + } + + /// Returns the type of the elements associated with the given key in a property map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[inline] + pub(crate) unsafe fn prop_get_type(self, map: &ffi::VSMap, key: *const c_char) -> c_char { + (self.handle.as_ref().propGetType)(map, key) + } + + /// Returns the size in bytes of a property of type ptData. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[inline] + pub(crate) unsafe fn prop_get_data_size( + self, + map: &ffi::VSMap, + key: *const c_char, + index: i32, + error: &mut i32, + ) -> i32 { + (self.handle.as_ref().propGetDataSize)(map, key, index, error) + } + + prop_get_something!(prop_get_int, propGetInt, i64); + prop_get_something!(prop_get_float, propGetFloat, f64); + prop_get_something!(prop_get_data, propGetData, *const c_char); + prop_get_something!(prop_get_node, propGetNode, *mut ffi::VSNodeRef); + prop_get_something!(prop_get_frame, propGetFrame, *const ffi::VSFrameRef); + prop_get_something!(prop_get_func, propGetFunc, *mut ffi::VSFuncRef); + + prop_set_something!(prop_set_int, propSetInt, i64); + prop_set_something!(prop_set_float, propSetFloat, f64); + prop_set_something!(prop_set_node, propSetNode, *mut ffi::VSNodeRef); + prop_set_something!(prop_set_frame, propSetFrame, *const ffi::VSFrameRef); + prop_set_something!(prop_set_func, propSetFunc, *mut ffi::VSFuncRef); + + /// Retrieves an array of integers from a map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn prop_get_int_array( + self, + map: &ffi::VSMap, + key: *const c_char, + error: &mut i32, + ) -> *const i64 { + (self.handle.as_ref().propGetIntArray)(map, key, error) + } + + /// Retrieves an array of floating point numbers from a map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn prop_get_float_array( + self, + map: &ffi::VSMap, + key: *const c_char, + error: &mut i32, + ) -> *const f64 { + (self.handle.as_ref().propGetFloatArray)(map, key, error) + } + + /// Adds a data property to the map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + /// + /// # Panics + /// Panics if `value.len()` can't fit in an `i32`. + #[inline] + pub(crate) unsafe fn prop_set_data( + self, + map: &mut ffi::VSMap, + key: *const c_char, + value: &[u8], + append: ffi::VSPropAppendMode, + ) -> i32 { + let length = value.len(); + assert!(length <= i32::max_value() as usize); + let length = length as i32; + + (self.handle.as_ref().propSetData)(map, key, value.as_ptr() as _, length, append as i32) + } + + /// Adds an array of integers to the map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + /// + /// # Panics + /// Panics if `value.len()` can't fit in an `i32`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn prop_set_int_array( + self, + map: &mut ffi::VSMap, + key: *const c_char, + value: &[i64], + ) -> i32 { + let length = value.len(); + assert!(length <= i32::max_value() as usize); + let length = length as i32; + + (self.handle.as_ref().propSetIntArray)(map, key, value.as_ptr(), length) + } + + /// Adds an array of floating point numbers to the map. + /// + /// # Safety + /// The caller must ensure `map` and `key` are valid. + /// + /// # Panics + /// Panics if `value.len()` can't fit in an `i32`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn prop_set_float_array( + self, + map: &mut ffi::VSMap, + key: *const c_char, + value: &[f64], + ) -> i32 { + let length = value.len(); + assert!(length <= i32::max_value() as usize); + let length = length as i32; + + (self.handle.as_ref().propSetFloatArray)(map, key, value.as_ptr(), length) + } + + /// Frees `function`. + /// + /// # Safety + /// The caller must ensure `function` is valid. + #[inline] + pub(crate) unsafe fn free_func(self, function: *mut ffi::VSFuncRef) { + (self.handle.as_ref().freeFunc)(function); + } + + /// Clones `function`. + /// + /// # Safety + /// The caller must ensure `function` is valid. + #[inline] + pub(crate) unsafe fn clone_func(self, function: *mut ffi::VSFuncRef) -> *mut ffi::VSFuncRef { + (self.handle.as_ref().cloneFuncRef)(function) + } + + /// Returns information about the VapourSynth core. + /// + /// # Safety + /// The caller must ensure `core` is valid. + #[inline] + pub(crate) unsafe fn get_core_info(self, core: *mut ffi::VSCore) -> *const ffi::VSCoreInfo { + (self.handle.as_ref().getCoreInfo)(core) + } + + /// Returns a VSFormat structure from a video format identifier. + /// + /// # Safety + /// The caller must ensure `core` is valid. + #[inline] + pub(crate) unsafe fn get_format_preset( + self, + id: i32, + core: *mut ffi::VSCore, + ) -> *const ffi::VSFormat { + (self.handle.as_ref().getFormatPreset)(id, core) + } + + /// Registers a custom video format. + /// + /// # Safety + /// The caller must ensure `core` is valid. + #[inline] + pub(crate) unsafe fn register_format( + self, + color_family: ffi::VSColorFamily, + sample_type: ffi::VSSampleType, + bits_per_sample: i32, + sub_sampling_w: i32, + sub_sampling_h: i32, + core: *mut ffi::VSCore, + ) -> *const ffi::VSFormat { + (self.handle.as_ref().registerFormat)( + color_family as i32, + sample_type as i32, + bits_per_sample, + sub_sampling_w, + sub_sampling_h, + core, + ) + } + + /// Creates a new filter node. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))] + #[inline] + pub(crate) unsafe fn create_filter( + self, + in_: *const ffi::VSMap, + out: *mut ffi::VSMap, + name: *const c_char, + init: ffi::VSFilterInit, + get_frame: ffi::VSFilterGetFrame, + free: ffi::VSFilterFree, + filter_mode: ffi::VSFilterMode, + flags: ffi::VSNodeFlags, + instance_data: *mut c_void, + core: *mut ffi::VSCore, + ) { + (self.handle.as_ref().createFilter)( + in_, + out, + name, + init, + get_frame, + free, + filter_mode as _, + flags.0, + instance_data, + core, + ); + } + + /// Sets node's video info. + /// + /// # Safety + /// The caller must ensure `node` is valid. + /// + /// # Panics + /// Panics if `vi.len()` can't fit in an `i32`. + #[inline] + pub(crate) unsafe fn set_video_info(self, vi: &[ffi::VSVideoInfo], node: *mut ffi::VSNode) { + let length = vi.len(); + assert!(length <= i32::max_value() as usize); + let length = length as i32; + + (self.handle.as_ref().setVideoInfo)(vi.as_ptr(), length, node); + } + + /// Adds an error message to a frame context, replacing the existing message, if any. + /// + /// This is the way to report errors in a filter's "get frame" function. Such errors are not + /// necessarily fatal, i.e. the caller can try to request the same frame again. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn set_filter_error( + self, + message: *const c_char, + frame_ctx: *mut ffi::VSFrameContext, + ) { + (self.handle.as_ref().setFilterError)(message, frame_ctx); + } + + /// Requests a frame from a node and returns immediately. + /// + /// This is only used in filters' "get frame" functions. + /// + /// # Safety + /// The caller must ensure all pointers are valid and this is called from a filter "get frame" + /// function. + #[inline] + pub(crate) unsafe fn request_frame_filter( + self, + n: i32, + node: *mut ffi::VSNodeRef, + frame_ctx: *mut ffi::VSFrameContext, + ) { + (self.handle.as_ref().requestFrameFilter)(n, node, frame_ctx); + } + + /// Retrieves a frame that was previously requested with `request_frame_filter()`. + /// + /// This is only used in filters' "get frame" functions. + /// + /// # Safety + /// The caller must ensure all pointers are valid and this is called from a filter "get frame" + /// function. + #[inline] + pub(crate) unsafe fn get_frame_filter( + self, + n: i32, + node: *mut ffi::VSNodeRef, + frame_ctx: *mut ffi::VSFrameContext, + ) -> *const ffi::VSFrameRef { + (self.handle.as_ref().getFrameFilter)(n, node, frame_ctx) + } + + /// Duplicates the frame (not just the reference). As the frame buffer is shared in a + /// copy-on-write fashion, the frame content is not really duplicated until a write operation + /// occurs. This is transparent for the user. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn copy_frame( + self, + f: &ffi::VSFrameRef, + core: *mut ffi::VSCore, + ) -> *mut ffi::VSFrameRef { + (self.handle.as_ref().copyFrame)(f, core) + } + + /// Creates a new frame, optionally copying the properties attached to another frame. The new + /// frame contains uninitialised memory. + /// + /// # Safety + /// The caller must ensure all pointers are valid and that the uninitialized plane data of the + /// returned frame is handled carefully. + #[inline] + pub(crate) unsafe fn new_video_frame( + self, + format: &ffi::VSFormat, + width: i32, + height: i32, + prop_src: *const ffi::VSFrameRef, + core: *mut ffi::VSCore, + ) -> *mut ffi::VSFrameRef { + (self.handle.as_ref().newVideoFrame)(format, width, height, prop_src, core) + } + + /// Returns a pointer to the plugin with the given identifier, or a null pointer if not found. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn get_plugin_by_id( + self, + identifier: *const c_char, + core: *mut ffi::VSCore, + ) -> *mut ffi::VSPlugin { + (self.handle.as_ref().getPluginById)(identifier, core) + } + + /// Returns a pointer to the plugin with the given namespace, or a null pointer if not found. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn get_plugin_by_ns( + self, + namespace: *const c_char, + core: *mut ffi::VSCore, + ) -> *mut ffi::VSPlugin { + (self.handle.as_ref().getPluginByNs)(namespace, core) + } + + /// Returns a map containing a list of all loaded plugins. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn get_plugins(self, core: *mut ffi::VSCore) -> *mut ffi::VSMap { + (self.handle.as_ref().getPlugins)(core) + } + + /// Returns a map containing a list of the filters exported by a plugin. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn get_functions(self, plugin: *mut ffi::VSPlugin) -> *mut ffi::VSMap { + (self.handle.as_ref().getFunctions)(plugin) + } + + /// Returns the absolute path to the plugin, including the plugin's file name. This is the real + /// location of the plugin, i.e. there are no symbolic links in the path. + /// + /// Path elements are always delimited with forward slashes. + /// + /// VapourSynth retains ownership of the returned pointer. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + // This was introduced in R25 without bumping the API version (R3) but we must be sure it's + // there, so require R3.1. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn get_plugin_path(self, plugin: *mut ffi::VSPlugin) -> *const c_char { + (self.handle.as_ref().getPluginPath)(plugin) + } + + /// Invokes a filter. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn invoke( + self, + plugin: *mut ffi::VSPlugin, + name: *const c_char, + args: *const ffi::VSMap, + ) -> *mut ffi::VSMap { + (self.handle.as_ref().invoke)(plugin, name, args) + } + + /// Returns the index of the node from which the frame is being requested. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn get_output_index(self, frame_ctx: *mut ffi::VSFrameContext) -> i32 { + (self.handle.as_ref().getOutputIndex)(frame_ctx) + } + + /// Creates a user-defined function. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn create_func( + self, + func: ffi::VSPublicFunction, + user_data: *mut c_void, + free: ffi::VSFreeFuncData, + core: *mut ffi::VSCore, + ) -> *mut ffi::VSFuncRef { + (self.handle.as_ref().createFunc)(func, user_data, free, core, self.handle.as_ptr()) + } + + /// Calls a function. If the call fails out will have an error set. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn call_func( + self, + func: *mut ffi::VSFuncRef, + in_: *const ffi::VSMap, + out: *mut ffi::VSMap, + ) { + (self.handle.as_ref().callFunc)(func, in_, out, ptr::null_mut(), ptr::null()); + } + + /// Registers a filter exported by the plugin. A plugin can export any number of filters. + /// + /// # Safety + /// The caller must ensure all pointers are valid. + #[inline] + pub(crate) unsafe fn register_function( + self, + name: *const c_char, + args: *const c_char, + args_func: ffi::VSPublicFunction, + function_data: *mut c_void, + plugin: *mut ffi::VSPlugin, + ) { + (self.handle.as_ref().registerFunction)(name, args, args_func, function_data, plugin); + } +} + +impl MessageType { + #[inline] + fn ffi_type(self) -> c_int { + let rv = match self { + MessageType::Debug => ffi::VSMessageType::mtDebug, + MessageType::Warning => ffi::VSMessageType::mtWarning, + MessageType::Critical => ffi::VSMessageType::mtCritical, + MessageType::Fatal => ffi::VSMessageType::mtFatal, + }; + rv as c_int + } + + #[inline] + fn from_ffi_type(x: c_int) -> Option { + match x { + x if x == ffi::VSMessageType::mtDebug as c_int => Some(MessageType::Debug), + x if x == ffi::VSMessageType::mtWarning as c_int => Some(MessageType::Warning), + x if x == ffi::VSMessageType::mtCritical as c_int => Some(MessageType::Critical), + x if x == ffi::VSMessageType::mtFatal as c_int => Some(MessageType::Fatal), + _ => None, + } + } +} diff --git a/vapoursynth/src/component.rs b/vapoursynth/src/component.rs new file mode 100644 index 0000000..c129562 --- /dev/null +++ b/vapoursynth/src/component.rs @@ -0,0 +1,52 @@ +//! The pixel component trait. + +#[cfg(feature = "f16-pixel-type")] +use half::f16; + +use format::{Format, SampleType}; + +/// A trait for possible pixel components. +/// +/// # Safety +/// Implementing this trait allows retrieving slices of pixel data from the frame for the target +/// type, so the target type must be valid for the given format. +pub unsafe trait Component { + /// Returns whether this component is valid for this format. + fn is_valid(format: Format) -> bool; +} + +unsafe impl Component for u8 { + #[inline] + fn is_valid(format: Format) -> bool { + format.sample_type() == SampleType::Integer && format.bytes_per_sample() == 1 + } +} + +unsafe impl Component for u16 { + #[inline] + fn is_valid(format: Format) -> bool { + format.sample_type() == SampleType::Integer && format.bytes_per_sample() == 2 + } +} + +unsafe impl Component for u32 { + #[inline] + fn is_valid(format: Format) -> bool { + format.sample_type() == SampleType::Integer && format.bytes_per_sample() == 4 + } +} + +#[cfg(feature = "f16-pixel-type")] +unsafe impl Component for f16 { + #[inline] + fn is_valid(format: Format) -> bool { + format.sample_type() == SampleType::Float && format.bytes_per_sample() == 2 + } +} + +unsafe impl Component for f32 { + #[inline] + fn is_valid(format: Format) -> bool { + format.sample_type() == SampleType::Float && format.bytes_per_sample() == 4 + } +} diff --git a/vapoursynth/src/core.rs b/vapoursynth/src/core.rs new file mode 100644 index 0000000..f3a3fdc --- /dev/null +++ b/vapoursynth/src/core.rs @@ -0,0 +1,184 @@ +//! VapourSynth cores. + +use std::ffi::{CStr, CString, NulError}; +use std::fmt; +use std::marker::PhantomData; +use std::ptr::NonNull; +use vapoursynth_sys as ffi; + +use api::API; +use format::{ColorFamily, Format, FormatID, SampleType}; +use map::OwnedMap; +use plugin::Plugin; + +/// Contains information about a VapourSynth core. +#[derive(Debug, Clone, Copy, Hash)] +pub struct Info { + /// String containing the name of the library, copyright notice, core and API versions. + pub version_string: &'static str, + + /// Version of the core. + pub core_version: i32, + + /// Version of the API. + pub api_version: i32, + + /// Number of worker threads. + pub num_threads: usize, + + /// The framebuffer cache will be allowed to grow up to this size (bytes) before memory is + /// aggressively reclaimed. + pub max_framebuffer_size: u64, + + /// Current size of the framebuffer cache, in bytes. + pub used_framebuffer_size: u64, +} + +/// A reference to a VapourSynth core. +#[derive(Debug, Clone, Copy)] +pub struct CoreRef<'a> { + handle: NonNull, + _owner: PhantomData<&'a ()>, +} + +unsafe impl<'a> Send for CoreRef<'a> {} +unsafe impl<'a> Sync for CoreRef<'a> {} + +impl<'a> CoreRef<'a> { + /// Wraps `handle` in a `CoreRef`. + /// + /// # Safety + /// The caller must ensure `handle` is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSCore) -> Self { + Self { + handle: NonNull::new_unchecked(handle), + _owner: PhantomData, + } + } + + /// Returns the underlying pointer. + #[inline] + pub(crate) fn ptr(&self) -> *mut ffi::VSCore { + self.handle.as_ptr() + } + + /// Returns information about the VapourSynth core. + pub fn info(self) -> Info { + let raw_info = unsafe { + API::get_cached() + .get_core_info(self.handle.as_ptr()) + .as_ref() + .unwrap() + }; + + let version_string = unsafe { CStr::from_ptr(raw_info.versionString).to_str().unwrap() }; + debug_assert!(raw_info.numThreads >= 0); + debug_assert!(raw_info.maxFramebufferSize >= 0); + debug_assert!(raw_info.usedFramebufferSize >= 0); + + Info { + version_string, + core_version: raw_info.core, + api_version: raw_info.api, + num_threads: raw_info.numThreads as usize, + max_framebuffer_size: raw_info.maxFramebufferSize as u64, + used_framebuffer_size: raw_info.usedFramebufferSize as u64, + } + } + + /// Retrieves a registered or preset `Format` by its id. The id can be of a previously + /// registered format, or one of the `PresetFormat`. + #[inline] + pub fn get_format(&self, id: FormatID) -> Option> { + let ptr = unsafe { API::get_cached().get_format_preset(id.0, self.handle.as_ptr()) }; + unsafe { ptr.as_ref().map(|p| Format::from_ptr(p)) } + } + + /// Registers a custom video format. + /// + /// Returns `None` if an invalid format is described. + /// + /// Registering compat formats is not allowed. Only certain privileged built-in filters are + /// allowed to handle compat formats. + /// + /// RGB formats are not allowed to be subsampled. + #[inline] + pub fn register_format( + &self, + color_family: ColorFamily, + sample_type: SampleType, + bits_per_sample: u8, + sub_sampling_w: u8, + sub_sampling_h: u8, + ) -> Option> { + unsafe { + API::get_cached() + .register_format( + color_family.into(), + sample_type.into(), + i32::from(bits_per_sample), + i32::from(sub_sampling_w), + i32::from(sub_sampling_h), + self.handle.as_ptr(), + ).as_ref() + .map(|p| Format::from_ptr(p)) + } + } + + /// Returns a plugin with the given identifier. + #[inline] + pub fn get_plugin_by_id(&self, id: &str) -> Result, NulError> { + let id = CString::new(id)?; + let ptr = unsafe { API::get_cached().get_plugin_by_id(id.as_ptr(), self.handle.as_ptr()) }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { Plugin::from_ptr(ptr) })) + } + } + + /// Returns a plugin with the given namespace. + /// + /// `get_plugin_by_id()` should be used instead. + #[inline] + pub fn get_plugin_by_namespace(&self, namespace: &str) -> Result, NulError> { + let namespace = CString::new(namespace)?; + let ptr = + unsafe { API::get_cached().get_plugin_by_ns(namespace.as_ptr(), self.handle.as_ptr()) }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { Plugin::from_ptr(ptr) })) + } + } + + /// Returns a map containing a list of all loaded plugins. + /// + /// Keys: meaningless unique strings; + /// + /// Values: namespace, identifier, and full name, separated by semicolons. + // TODO: parse the values on the crate side and return a nice struct. + #[inline] + pub fn plugins(&self) -> OwnedMap { + unsafe { OwnedMap::from_ptr(API::get_cached().get_plugins(self.handle.as_ptr())) } + } +} + +impl fmt::Display for Info { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.version_string)?; + writeln!(f, "Worker threads: {}", self.num_threads)?; + writeln!( + f, + "Max framebuffer cache size: {}", + self.max_framebuffer_size + )?; + writeln!( + f, + "Current framebuffer cache size: {}", + self.used_framebuffer_size + ) + } +} diff --git a/vapoursynth/src/format.rs b/vapoursynth/src/format.rs new file mode 100644 index 0000000..8661fca --- /dev/null +++ b/vapoursynth/src/format.rs @@ -0,0 +1,256 @@ +//! VapourSynth frame formats. + +use std::ffi::CStr; +use std::fmt::{self, Display}; +use std::marker::PhantomData; +use std::ops::Deref; +use vapoursynth_sys as ffi; + +/// Contains information about a video format. +#[derive(Debug, Clone, Copy)] +pub struct Format<'core> { + handle: &'core ffi::VSFormat, +} + +/// Preset VapourSynth formats. +/// +/// The presets suffixed with H and S have floating point sample type. The H and S suffixes stand +/// for half precision and single precision, respectively. +/// +/// The compat formats are the only packed formats in VapourSynth. Everything else is planar. They +/// exist for compatibility with Avisynth plugins. They are not to be implemented in native +/// VapourSynth plugins. +#[cfg_attr(feature = "cargo-clippy", allow(unreadable_literal))] +#[repr(i32)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum PresetFormat { + Gray8 = 1000010, + Gray16 = 1000011, + GrayH = 1000012, + GrayS = 1000013, + YUV420P8 = 3000010, + YUV422P8 = 3000011, + YUV444P8 = 3000012, + YUV410P8 = 3000013, + YUV411P8 = 3000014, + YUV440P8 = 3000015, + YUV420P9 = 3000016, + YUV422P9 = 3000017, + YUV444P9 = 3000018, + YUV420P10 = 3000019, + YUV422P10 = 3000020, + YUV444P10 = 3000021, + YUV420P16 = 3000022, + YUV422P16 = 3000023, + YUV444P16 = 3000024, + YUV444PH = 3000025, + YUV444PS = 3000026, + YUV420P12 = 3000027, + YUV422P12 = 3000028, + YUV444P12 = 3000029, + YUV420P14 = 3000030, + YUV422P14 = 3000031, + YUV444P14 = 3000032, + RGB24 = 2000010, + RGB27 = 2000011, + RGB30 = 2000012, + RGB48 = 2000013, + RGBH = 2000014, + RGBS = 2000015, + CompatBGR32 = 9000010, + CompatYUY2 = 9000011, +} + +/// Format color families. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ColorFamily { + Gray, + RGB, + YUV, + YCoCg, + Compat, +} + +/// Format sample types. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum SampleType { + Integer, + Float, +} + +/// A unique format identifier. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct FormatID(pub(crate) i32); + +impl<'core> PartialEq for Format<'core> { + #[inline] + fn eq(&self, other: &Format<'core>) -> bool { + self.id() == other.id() + } +} + +impl<'core> Eq for Format<'core> {} + +#[doc(hidden)] +impl<'core> Deref for Format<'core> { + type Target = ffi::VSFormat; + + // Technically this should return `&'core`. + #[inline] + fn deref(&self) -> &Self::Target { + self.handle + } +} + +impl<'core> Format<'core> { + /// Wraps a raw pointer in a `Format`. + /// + /// # Safety + /// The caller must ensure `ptr` and the lifetime is valid. + #[inline] + pub(crate) unsafe fn from_ptr(ptr: *const ffi::VSFormat) -> Self { + Self { handle: &*ptr } + } + + /// Gets the unique identifier of this format. + #[inline] + pub fn id(self) -> FormatID { + FormatID(self.handle.id) + } + + /// Gets the printable name of this format. + #[inline] + pub fn name(self) -> &'core str { + unsafe { CStr::from_ptr(&self.handle.name as _).to_str().unwrap() } + } + + /// Gets the number of planes of this format. + #[inline] + pub fn plane_count(self) -> usize { + let plane_count = self.handle.numPlanes; + debug_assert!(plane_count >= 0); + plane_count as usize + } + + /// Gets the color family of this format. + #[inline] + pub fn color_family(self) -> ColorFamily { + match self.handle.colorFamily { + x if x == ffi::VSColorFamily::cmGray as i32 => ColorFamily::Gray, + x if x == ffi::VSColorFamily::cmRGB as i32 => ColorFamily::RGB, + x if x == ffi::VSColorFamily::cmYUV as i32 => ColorFamily::YUV, + x if x == ffi::VSColorFamily::cmYCoCg as i32 => ColorFamily::YCoCg, + x if x == ffi::VSColorFamily::cmCompat as i32 => ColorFamily::Compat, + _ => unreachable!(), + } + } + + /// Gets the sample type of this format. + #[inline] + pub fn sample_type(self) -> SampleType { + match self.handle.sampleType { + x if x == ffi::VSSampleType::stInteger as i32 => SampleType::Integer, + x if x == ffi::VSSampleType::stFloat as i32 => SampleType::Float, + _ => unreachable!(), + } + } + + /// Gets the number of significant bits per sample. + #[inline] + pub fn bits_per_sample(self) -> u8 { + let rv = self.handle.bitsPerSample; + debug_assert!(rv >= 0 && rv <= i32::from(u8::max_value())); + rv as u8 + } + + /// Gets the number of bytes needed for a sample. This is always a power of 2 and the smallest + /// possible that can fit the number of bits used per sample. + #[inline] + pub fn bytes_per_sample(self) -> u8 { + let rv = self.handle.bytesPerSample; + debug_assert!(rv >= 0 && rv <= i32::from(u8::max_value())); + rv as u8 + } + + /// log2 subsampling factor, applied to second and third plane. + #[inline] + pub fn sub_sampling_w(self) -> u8 { + let rv = self.handle.subSamplingW; + debug_assert!(rv >= 0 && rv <= i32::from(u8::max_value())); + rv as u8 + } + + /// log2 subsampling factor, applied to second and third plane. + #[inline] + pub fn sub_sampling_h(self) -> u8 { + let rv = self.handle.subSamplingH; + debug_assert!(rv >= 0 && rv <= i32::from(u8::max_value())); + rv as u8 + } +} + +impl From for FormatID { + fn from(x: PresetFormat) -> Self { + FormatID(x as i32) + } +} + +#[doc(hidden)] +impl From for ffi::VSColorFamily { + #[inline] + fn from(x: ColorFamily) -> Self { + match x { + ColorFamily::Gray => ffi::VSColorFamily::cmGray, + ColorFamily::RGB => ffi::VSColorFamily::cmRGB, + ColorFamily::YUV => ffi::VSColorFamily::cmYUV, + ColorFamily::YCoCg => ffi::VSColorFamily::cmYCoCg, + ColorFamily::Compat => ffi::VSColorFamily::cmCompat, + } + } +} + +#[doc(hidden)] +impl From for ffi::VSSampleType { + #[inline] + fn from(x: SampleType) -> Self { + match x { + SampleType::Integer => ffi::VSSampleType::stInteger, + SampleType::Float => ffi::VSSampleType::stFloat, + } + } +} + +impl Display for ColorFamily { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!( + f, + "{}", + match *self { + ColorFamily::Gray => "Gray", + ColorFamily::RGB => "RGB", + ColorFamily::YUV => "YUV", + ColorFamily::YCoCg => "YCoCg", + ColorFamily::Compat => "Compat", + } + ) + } +} + +impl Display for SampleType { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!( + f, + "{}", + match *self { + SampleType::Integer => "Integer", + SampleType::Float => "Float", + } + ) + } +} + +impl From for FormatID { + fn from(x: i32) -> Self { + FormatID(x) + } +} diff --git a/vapoursynth/src/frame.rs b/vapoursynth/src/frame.rs new file mode 100644 index 0000000..3627a5c --- /dev/null +++ b/vapoursynth/src/frame.rs @@ -0,0 +1,500 @@ +//! VapourSynth frames. + +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr::{self, NonNull}; +use std::{mem, slice}; +use vapoursynth_sys as ffi; + +use api::API; +use component::Component; +use core::CoreRef; +use format::Format; +use map::{MapRef, MapRefMut}; +use video_info::Resolution; + +/// An error indicating that the frame data has non-zero padding. +#[derive(Fail, Debug, Clone, Copy, Eq, PartialEq)] +#[fail(display = "Frame data has non-zero padding: {}", _0)] +pub struct NonZeroPadding(usize); + +/// One frame of a clip. +// This type is intended to be publicly used only in reference form. +#[derive(Debug)] +pub struct Frame<'core> { + // The actual mutability of this depends on whether it's accessed via `&Frame` or `&mut Frame`. + handle: NonNull, + // The cached frame format for fast access. + format: Format<'core>, + _owner: PhantomData<&'core ()>, +} + +/// A reference to a ref-counted frame. +#[derive(Debug)] +pub struct FrameRef<'core> { + // Only immutable references to this are allowed. + frame: Frame<'core>, +} + +/// A reference to a mutable frame. +#[derive(Debug)] +pub struct FrameRefMut<'core> { + // Both mutable and immutable references to this are allowed. + frame: Frame<'core>, +} + +unsafe impl<'core> Send for Frame<'core> {} +unsafe impl<'core> Sync for Frame<'core> {} + +#[doc(hidden)] +impl<'core> Deref for Frame<'core> { + type Target = ffi::VSFrameRef; + + // Technically this should return `&'core`. + #[inline] + fn deref(&self) -> &Self::Target { + unsafe { self.handle.as_ref() } + } +} + +#[doc(hidden)] +impl<'core> DerefMut for Frame<'core> { + // Technically this should return `&'core`. + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { self.handle.as_mut() } + } +} + +impl<'core> Drop for Frame<'core> { + #[inline] + fn drop(&mut self) { + unsafe { + API::get_cached().free_frame(&self); + } + } +} + +impl<'core> Clone for FrameRef<'core> { + #[inline] + fn clone(&self) -> Self { + unsafe { + let handle = API::get_cached().clone_frame(self); + Self { + frame: Frame::from_ptr(handle), + } + } + } +} + +impl<'core> Deref for FrameRef<'core> { + type Target = Frame<'core>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.frame + } +} + +impl<'core> Deref for FrameRefMut<'core> { + type Target = Frame<'core>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.frame + } +} + +impl<'core> DerefMut for FrameRefMut<'core> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.frame + } +} + +impl<'core> FrameRef<'core> { + /// Wraps `handle` in a `FrameRef`. + /// + /// # Safety + /// The caller must ensure `handle` and the lifetime is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *const ffi::VSFrameRef) -> Self { + Self { + frame: Frame::from_ptr(handle), + } + } +} + +impl<'core> FrameRefMut<'core> { + /// Wraps `handle` in a `FrameRefMut`. + /// + /// # Safety + /// The caller must ensure `handle` and the lifetime is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSFrameRef) -> Self { + Self { + frame: Frame::from_ptr(handle), + } + } + + /// Creates a copy of the given frame. + /// + /// The plane data is copy-on-write, so this isn't very expensive by itself. + /// + /// Judging by the underlying implementation, it seems that any valid `core` can be used. + #[inline] + pub fn copy_of(core: CoreRef, frame: &Frame<'core>) -> Self { + Self { + frame: unsafe { Frame::from_ptr(API::get_cached().copy_frame(frame, core.ptr())) }, + } + } + + /// Creates a new frame with uninitialized plane data. + /// + /// Optionally copies the frame properties from the provided `prop_src` frame. + /// + /// # Safety + /// The returned frame contains uninitialized plane data. This should be handled carefully. See + /// the docs for `std::mem::uninitialized()` for more information. + /// + /// # Panics + /// Panics if the given resolution has components that don't fit into an `i32`. + #[inline] + pub unsafe fn new_uninitialized( + core: CoreRef<'core>, + prop_src: Option<&Frame<'core>>, + format: Format<'core>, + resolution: Resolution, + ) -> Self { + assert!(resolution.width <= i32::max_value() as usize); + assert!(resolution.height <= i32::max_value() as usize); + + Self { + frame: unsafe { + Frame::from_ptr(API::get_cached().new_video_frame( + &format, + resolution.width as i32, + resolution.height as i32, + prop_src.map(|f| f.deref() as _).unwrap_or(ptr::null()), + core.ptr(), + )) + }, + } + } +} + +impl<'core> From> for FrameRef<'core> { + #[inline] + fn from(x: FrameRefMut<'core>) -> Self { + Self { frame: x.frame } + } +} + +impl<'core> Frame<'core> { + /// Converts a pointer to a frame to a reference. + /// + /// # Safety + /// The caller needs to ensure the pointer and the lifetime is valid, and that the resulting + /// `Frame` gets put into `FrameRef` or `FrameRefMut` according to the input pointer + /// mutability. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *const ffi::VSFrameRef) -> Self { + Self { + handle: NonNull::new_unchecked(handle as *mut ffi::VSFrameRef), + format: unsafe { + let ptr = API::get_cached().get_frame_format(&*handle); + Format::from_ptr(ptr) + }, + _owner: PhantomData, + } + } + + /// Returns the frame format. + #[inline] + pub fn format(&self) -> Format<'core> { + self.format + } + + /// Returns the width of a plane, in pixels. + /// + /// The width depends on the plane number because of the possible chroma subsampling. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()`. + #[inline] + pub fn width(&self, plane: usize) -> usize { + assert!(plane < self.format().plane_count()); + + unsafe { API::get_cached().get_frame_width(self, plane as i32) as usize } + } + + /// Returns the height of a plane, in pixels. + /// + /// The height depends on the plane number because of the possible chroma subsampling. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()`. + #[inline] + pub fn height(&self, plane: usize) -> usize { + assert!(plane < self.format().plane_count()); + + unsafe { API::get_cached().get_frame_height(self, plane as i32) as usize } + } + + /// Returns the resolution of a plane. + /// + /// The resolution depends on the plane number because of the possible chroma subsampling. + /// + /// # Panics + /// Panics if `plane` is invalid for this frame. + #[inline] + pub fn resolution(&self, plane: usize) -> Resolution { + assert!(plane < self.format().plane_count()); + + Resolution { + width: self.width(plane), + height: self.height(plane), + } + } + + /// Returns the distance in bytes between two consecutive lines of a plane. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()`. + #[inline] + pub fn stride(&self, plane: usize) -> usize { + assert!(plane < self.format().plane_count()); + + unsafe { API::get_cached().get_frame_stride(self, plane as i32) as usize } + } + + /// Returns a slice of a plane's pixel row. + /// + /// # Panics + /// Panics if the requested plane, row or component type is invalid. + #[inline] + pub fn plane_row(&self, plane: usize, row: usize) -> &[T] { + assert!(plane < self.format().plane_count()); + assert!(row < self.height(plane)); + assert!(T::is_valid(self.format())); + + let stride = self.stride(plane); + let ptr = self.data_ptr(plane); + + let offset = stride * row; + assert!(offset <= isize::max_value() as usize); + let offset = offset as isize; + + let row_ptr = unsafe { ptr.offset(offset) }; + let width = self.width(plane); + + unsafe { slice::from_raw_parts(row_ptr as *const T, width) } + } + + /// Returns a mutable slice of a plane's pixel row. + /// + /// # Panics + /// Panics if the requested plane, row or component type is invalid. + #[inline] + pub fn plane_row_mut(&mut self, plane: usize, row: usize) -> &mut [T] { + assert!(plane < self.format().plane_count()); + assert!(row < self.height(plane)); + assert!(T::is_valid(self.format())); + + let stride = self.stride(plane); + let ptr = self.data_ptr_mut(plane); + + let offset = stride * row; + assert!(offset <= isize::max_value() as usize); + let offset = offset as isize; + + let row_ptr = unsafe { ptr.offset(offset) }; + let width = self.width(plane); + + unsafe { slice::from_raw_parts_mut(row_ptr as *mut T, width) } + } + + /// Returns a slice of the plane's pixels. + /// + /// The length of the returned slice is `height() * width()`. If the pixel data has non-zero + /// padding (that is, `stride()` is larger than `width()`), an error is returned, since + /// returning the data slice would open access to uninitialized bytes. + /// + /// # Panics + /// Panics if the requested plane or component type is invalid. + pub fn plane(&self, plane: usize) -> Result<&[T], NonZeroPadding> { + assert!(plane < self.format().plane_count()); + assert!(T::is_valid(self.format())); + + let stride = self.stride(plane); + let width_in_bytes = self.width(plane) * usize::from(self.format().bytes_per_sample()); + if stride != width_in_bytes { + return Err(NonZeroPadding(stride - width_in_bytes)); + } + + let height = self.height(plane); + let length = height * self.width(plane); + let ptr = self.data_ptr(plane); + + Ok(unsafe { slice::from_raw_parts(ptr as *const T, length) }) + } + + /// Returns a mutable slice of the plane's pixels. + /// + /// The length of the returned slice is `height() * width()`. If the pixel data has non-zero + /// padding (that is, `stride()` is larger than `width()`), an error is returned, since + /// returning the data slice would open access to uninitialized bytes. + /// + /// # Panics + /// Panics if the requested plane or component type is invalid. + pub fn plane_mut(&mut self, plane: usize) -> Result<&mut [T], NonZeroPadding> { + assert!(plane < self.format().plane_count()); + assert!(T::is_valid(self.format())); + + let stride = self.stride(plane); + let width_in_bytes = self.width(plane) * usize::from(self.format().bytes_per_sample()); + if stride != width_in_bytes { + return Err(NonZeroPadding(stride - width_in_bytes)); + } + + let height = self.height(plane); + let length = height * self.width(plane); + let ptr = self.data_ptr_mut(plane); + + Ok(unsafe { slice::from_raw_parts_mut(ptr as *mut T, length) }) + } + + /// Returns a pointer to the plane's pixels. + /// + /// The pointer points to an array with a length of `height() * stride()` and is valid for as + /// long as the frame is alive. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()`. + #[inline] + pub fn data_ptr(&self, plane: usize) -> *const u8 { + assert!(plane < self.format().plane_count()); + + unsafe { API::get_cached().get_frame_read_ptr(self, plane as i32) } + } + + /// Returns a mutable pointer to the plane's pixels. + /// + /// The pointer points to an array with a length of `height() * stride()` and is valid for as + /// long as the frame is alive. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()`. + #[inline] + pub fn data_ptr_mut(&mut self, plane: usize) -> *mut u8 { + assert!(plane < self.format().plane_count()); + + unsafe { API::get_cached().get_frame_write_ptr(self, plane as i32) } + } + + /// Returns a slice of a plane's pixel row. + /// + /// The length of the returned slice is equal to `width() * format().bytes_per_sample()`. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()` or if `row >= height()`. + pub fn data_row(&self, plane: usize, row: usize) -> &[u8] { + assert!(plane < self.format().plane_count()); + assert!(row < self.height(plane)); + + let stride = self.stride(plane); + let ptr = self.data_ptr(plane); + + let offset = stride * row; + assert!(offset <= isize::max_value() as usize); + let offset = offset as isize; + + let row_ptr = unsafe { ptr.offset(offset) }; + let width = self.width(plane) * usize::from(self.format().bytes_per_sample()); + + unsafe { slice::from_raw_parts(row_ptr, width) } + } + + /// Returns a mutable slice of a plane's pixel row. + /// + /// The length of the returned slice is equal to `width() * format().bytes_per_sample()`. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()` or if `row >= height()`. + pub fn data_row_mut(&mut self, plane: usize, row: usize) -> &mut [u8] { + assert!(plane < self.format().plane_count()); + assert!(row < self.height(plane)); + + let stride = self.stride(plane); + let ptr = self.data_ptr_mut(plane); + + let offset = stride * row; + assert!(offset <= isize::max_value() as usize); + let offset = offset as isize; + + let row_ptr = unsafe { ptr.offset(offset) }; + let width = self.width(plane) * usize::from(self.format().bytes_per_sample()); + + unsafe { slice::from_raw_parts_mut(row_ptr, width) } + } + + /// Returns a slice of the plane's pixels. + /// + /// The length of the returned slice is `height() * width() * format().bytes_per_sample()`. If + /// the pixel data has non-zero padding (that is, `stride()` is larger than `width()`), an + /// error is returned, since returning the data slice would open access to uninitialized bytes. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()` or if `row >= height()`. + pub fn data(&self, plane: usize) -> Result<&[u8], NonZeroPadding> { + assert!(plane < self.format().plane_count()); + + let stride = self.stride(plane); + let width = self.width(plane) * usize::from(self.format().bytes_per_sample()); + if stride != width { + return Err(NonZeroPadding(stride - width)); + } + + let height = self.height(plane); + let length = height * stride; + let ptr = self.data_ptr(plane); + + Ok(unsafe { slice::from_raw_parts(ptr, length) }) + } + + /// Returns a mutable slice of the plane's pixels. + /// + /// The length of the returned slice is `height() * width() * format().bytes_per_sample()`. If + /// the pixel data has non-zero padding (that is, `stride()` is larger than `width()`), an + /// error is returned, since returning the data slice would open access to uninitialized bytes. + /// + /// # Panics + /// Panics if `plane >= format().plane_count()` or if `row >= height()`. + pub fn data_mut(&mut self, plane: usize) -> Result<&mut [u8], NonZeroPadding> { + assert!(plane < self.format().plane_count()); + + let stride = self.stride(plane); + let width = self.width(plane) * usize::from(self.format().bytes_per_sample()); + if stride != width { + return Err(NonZeroPadding(stride - width)); + } + + let height = self.height(plane); + let length = height * stride; + let ptr = self.data_ptr_mut(plane); + + Ok(unsafe { slice::from_raw_parts_mut(ptr, length) }) + } + + /// Returns a map of frame's properties. + #[inline] + pub fn props(&self) -> MapRef { + unsafe { MapRef::from_ptr(API::get_cached().get_frame_props_ro(self)) } + } + + /// Returns a mutable map of frame's properties. + #[inline] + pub fn props_mut(&mut self) -> MapRefMut { + unsafe { MapRefMut::from_ptr(API::get_cached().get_frame_props_rw(self)) } + } +} diff --git a/vapoursynth/src/function.rs b/vapoursynth/src/function.rs new file mode 100644 index 0000000..ae80223 --- /dev/null +++ b/vapoursynth/src/function.rs @@ -0,0 +1,123 @@ +//! VapourSynth callable functions. + +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +use std::ptr::NonNull; +use std::{mem, panic, process}; +use vapoursynth_sys as ffi; + +use api::API; +use core::CoreRef; +use map::{Map, MapRef, MapRefMut}; + +/// Holds a reference to a function that may be called. +#[derive(Debug)] +pub struct Function<'core> { + handle: NonNull, + _owner: PhantomData<&'core ()>, +} + +unsafe impl<'core> Send for Function<'core> {} +unsafe impl<'core> Sync for Function<'core> {} + +impl<'core> Drop for Function<'core> { + #[inline] + fn drop(&mut self) { + unsafe { + API::get_cached().free_func(self.handle.as_ptr()); + } + } +} + +impl<'core> Clone for Function<'core> { + #[inline] + fn clone(&self) -> Self { + let handle = unsafe { API::get_cached().clone_func(self.handle.as_ptr()) }; + Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + _owner: PhantomData, + } + } +} + +impl<'core> Function<'core> { + /// Wraps `handle` in a `Function`. + /// + /// # Safety + /// The caller must ensure `handle` and the lifetime are valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSFuncRef) -> Self { + Self { + handle: NonNull::new_unchecked(handle), + _owner: PhantomData, + } + } + + /// Returns the underlying pointer. + #[inline] + pub(crate) fn ptr(&self) -> *mut ffi::VSFuncRef { + self.handle.as_ptr() + } + + /// Creates a new function. + /// + /// To indicate an error from the callback, set an error on the output map. + #[inline] + pub fn new(api: API, core: CoreRef<'core>, callback: F) -> Self + where + F: Fn(API, CoreRef<'core>, &Map<'core>, &mut Map<'core>) + Send + Sync + 'core, + { + unsafe extern "system" fn c_callback<'core, F>( + in_: *const ffi::VSMap, + out: *mut ffi::VSMap, + user_data: *mut c_void, + core: *mut ffi::VSCore, + _vsapi: *const ffi::VSAPI, + ) where + F: Fn(API, CoreRef<'core>, &Map<'core>, &mut Map<'core>) + Send + Sync + 'core, + { + let closure = move || { + let api = API::get_cached(); + let core = CoreRef::from_ptr(core); + let in_ = MapRef::from_ptr(in_); + let mut out = MapRefMut::from_ptr(out); + let callback = Box::from_raw(user_data as *mut F); + + callback(api, core, &in_, &mut out); + + mem::forget(callback); + }; + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } + } + + unsafe extern "system" fn c_free(user_data: *mut c_void) { + drop(Box::from_raw(user_data as *mut F)) + } + + let data = Box::new(callback); + + let handle = unsafe { + API::get_cached().create_func( + c_callback::<'core, F>, + Box::into_raw(data) as _, + Some(c_free::), + core.ptr(), + ) + }; + + Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + _owner: PhantomData, + } + } + + /// Calls the function. If the call fails `out` will have an error set. + #[inline] + pub fn call(&self, in_: &Map<'core>, out: &mut Map<'core>) { + unsafe { API::get_cached().call_func(self.handle.as_ptr(), in_.deref(), out.deref_mut()) }; + } +} diff --git a/vapoursynth/src/lib.rs b/vapoursynth/src/lib.rs new file mode 100644 index 0000000..d775875 --- /dev/null +++ b/vapoursynth/src/lib.rs @@ -0,0 +1,213 @@ +//! A safe wrapper for [VapourSynth](https://github.com/vapoursynth/vapoursynth), written in Rust. +//! +//! The primary goal is safety (that is, safe Rust code should not trigger undefined behavior), and +//! secondary goals include performance and ease of use. +//! +//! ## Functionality +//! +//! Most of the VapourSynth API is covered. It's possible to evaluate `.vpy` scripts, access their +//! properties and output, retrieve frames; enumerate loaded plugins and invoke their functions as +//! well as create VapourSynth filters. +//! +//! For an example usage see +//! [examples/vspipe.rs](https://github.com/YaLTeR/vapoursynth-rs/blob/master/vapoursynth/examples/vspipe.rs), +//! a complete reimplementation of VapourSynth's +//! [vspipe](https://github.com/vapoursynth/vapoursynth/blob/master/src/vspipe/vspipe.cpp) in safe +//! Rust utilizing this crate. +//! +//! For a VapourSynth plugin example see +//! [sample-plugin](https://github.com/YaLTeR/vapoursynth-rs/blob/master/sample-plugin) which +//! implements some simple filters. +//! +//! ## Short example +//! +//! ```no_run +//! # extern crate failure; +//! # extern crate vapoursynth; +//! # use failure::Error; +//! # #[cfg(all(feature = "vsscript-functions", +//! # feature = "gte-vsscript-api-31", +//! # any(feature = "vapoursynth-functions", feature = "gte-vsscript-api-32")))] +//! # fn foo() -> Result<(), Error> { +//! use vapoursynth::prelude::*; +//! +//! let env = Environment::from_file("test.vpy", EvalFlags::SetWorkingDir)?; +//! let node = env.get_output(0)?.0; // Without `.0` for VSScript API 3.0 +//! let frame = node.get_frame(0)?; +//! +//! println!("Resolution: {}×{}", frame.width(0), frame.height(0)); +//! # Ok(()) +//! # } +//! # fn main() { +//! # } +//! ``` +//! +//! ## Plugins +//! +//! To make a VapourSynth plugin, start by creating a new Rust library with +//! `crate-type = ["cdylib"]`. Then add filters by implementing the `plugins::Filter` trait. Bind +//! them to functions by implementing `plugins::FilterFunction`, which is much more easily done via +//! the `make_filter_function!` macro. Finally, put `export_vapoursynth_plugin!` at the top level +//! of `src/lib.rs` to export the functionality. +//! +//! **Important note:** due to what seems to be a +//! [bug](https://github.com/rust-lang/rust/issues/50176) in rustc, it's impossible to make plugins +//! on the `i686-pc-windows-gnu` target (all other variations of `x86_64` and `i686` do work). +//! Please use `i686-pc-windows-msvc` for an i686 Windows plugin. +//! +//! ## Short plugin example +//! +//! ```no_run +//! #[macro_use] +//! extern crate failure; +//! #[macro_use] +//! extern crate vapoursynth; +//! +//! use failure::Error; +//! use vapoursynth::prelude::*; +//! use vapoursynth::core::CoreRef; +//! use vapoursynth::plugins::{Filter, FilterArgument, FrameContext, Metadata}; +//! use vapoursynth::video_info::VideoInfo; +//! +//! // A simple filter that passes the frames through unchanged. +//! struct Passthrough<'core> { +//! source: Node<'core>, +//! } +//! +//! impl<'core> Filter<'core> for Passthrough<'core> { +//! fn video_info(&self, _api: API, _core: CoreRef<'core>) -> Vec> { +//! vec![self.source.info()] +//! } +//! +//! fn get_frame_initial( +//! &self, +//! _api: API, +//! _core: CoreRef<'core>, +//! context: FrameContext, +//! n: usize, +//! ) -> Result>, Error> { +//! self.source.request_frame_filter(context, n); +//! Ok(None) +//! } +//! +//! fn get_frame( +//! &self, +//! _api: API, +//! _core: CoreRef<'core>, +//! context: FrameContext, +//! n: usize, +//! ) -> Result, Error> { +//! self.source +//! .get_frame_filter(context, n) +//! .ok_or(format_err!("Couldn't get the source frame")) +//! } +//! } +//! +//! make_filter_function! { +//! PassthroughFunction, "Passthrough" +//! +//! fn create_passthrough<'core>( +//! _api: API, +//! _core: CoreRef<'core>, +//! clip: Node<'core>, +//! ) -> Result + 'core>>, Error> { +//! Ok(Some(Box::new(Passthrough { source: clip }))) +//! } +//! } +//! +//! export_vapoursynth_plugin! { +//! Metadata { +//! identifier: "com.example.passthrough", +//! namespace: "passthrough", +//! name: "Example Plugin", +//! read_only: true, +//! }, +//! [PassthroughFunction::new()] +//! } +//! # fn main() { +//! # } +//! ``` +//! +//! Check [sample-plugin](https://github.com/YaLTeR/vapoursynth-rs/blob/master/sample-plugin) for +//! an example plugin which exports some simple filters. +//! +//! ## Supported Versions +//! +//! All VapourSynth and VSScript API versions starting with 3.0 are supported. By default the +//! crates use the 3.0 feature set. To enable higher API version support, enable one of the +//! following Cargo features: +//! +//! * `vapoursynth-api-31` for VapourSynth API 3.1 +//! * `vapoursynth-api-32` for VapourSynth API 3.2 +//! * `vapoursynth-api-33` for VapourSynth API 3.3 +//! * `vapoursynth-api-34` for VapourSynth API 3.4 +//! * `vapoursynth-api-35` for VapourSynth API 3.5 +//! * `vsscript-api-31` for VSScript API 3.1 +//! * `vsscript-api-32` for VSScript API 3.2 +//! +//! To enable linking to VapourSynth or VSScript functions, enable the following Cargo features: +//! +//! * `vapoursynth-functions` for VapourSynth functions (`getVapourSynthAPI()`) +//! * `vsscript-functions` for VSScript functions (`vsscript_*()`) +//! +//! ## Building +//! +//! Make sure you have the corresponding libraries available if you enable the linking features. +//! You can use the `VAPOURSYNTH_LIB_DIR` environment variable to specify a custom directory with +//! the library files. +//! +//! On Windows the easiest way is to use the VapourSynth installer (make sure the VapourSynth SDK +//! is checked). The crate should pick up the library directory automatically. If it doesn't or if +//! you're cross-compiling, set `VAPOURSYNTH_LIB_DIR` to +//! `\sdk\lib64` or `<...>\lib32`, depending on the target +//! bitness. + +#![doc(html_root_url = "https://docs.rs/vapoursynth/0.2.0")] +// Preventing all those warnings with #[cfg] directives would be really diffucult. +#![allow(unused, dead_code)] + +#[macro_use] +extern crate bitflags; +extern crate failure; +#[macro_use] +extern crate failure_derive; +#[cfg(feature = "f16-pixel-type")] +extern crate half; +#[cfg(any(not(feature = "gte-vsscript-api-32"), test))] +#[macro_use] +extern crate lazy_static; +extern crate vapoursynth_sys; + +#[cfg(feature = "vsscript-functions")] +pub mod vsscript; + +pub mod api; +pub mod component; +pub mod core; +pub mod format; +pub mod frame; +pub mod function; +pub mod map; +pub mod node; +pub mod plugin; +pub mod plugins; +pub mod video_info; + +pub mod prelude { + //! The VapourSynth prelude. + //! + //! Contains the types you most likely want to import anyway. + pub use super::api::{MessageType, API}; + pub use super::component::Component; + pub use super::format::{ColorFamily, PresetFormat, SampleType}; + pub use super::frame::{Frame, FrameRef, FrameRefMut}; + pub use super::map::{Map, OwnedMap, ValueType}; + pub use super::node::{GetFrameError, Node}; + pub use super::plugin::Plugin; + pub use super::video_info::Property; + + #[cfg(feature = "vsscript-functions")] + pub use super::vsscript::{self, Environment, EvalFlags}; +} + +mod tests; diff --git a/vapoursynth/src/map/errors.rs b/vapoursynth/src/map/errors.rs new file mode 100644 index 0000000..7147761 --- /dev/null +++ b/vapoursynth/src/map/errors.rs @@ -0,0 +1,44 @@ +use std::ffi::NulError; +use std::result; + +/// The error type for `Map` operations. +#[derive(Fail, Debug, Eq, PartialEq)] +pub enum Error { + #[fail(display = "The requested key wasn't found in the map")] + KeyNotFound, + #[fail(display = "The requested index was out of bounds")] + IndexOutOfBounds, + #[fail(display = "The given/requested value type doesn't match the type of the property")] + WrongValueType, + #[fail(display = "The key is invalid")] + InvalidKey(#[cause] InvalidKeyError), + #[fail(display = "Couldn't convert to a CString")] + CStringConversion(#[cause] NulError), +} + +/// A specialized `Result` type for `Map` operations. +pub type Result = result::Result; + +/// An error indicating the map key is invalid. +#[derive(Fail, Debug, Eq, PartialEq)] +#[cfg_attr(rustfmt, rustfmt_skip)] +pub enum InvalidKeyError { + #[fail(display = "The key is empty")] + EmptyKey, + #[fail(display = "The key contains an invalid character at index {}", _0)] + InvalidCharacter(usize), +} + +impl From for Error { + #[inline] + fn from(x: InvalidKeyError) -> Self { + Error::InvalidKey(x) + } +} + +impl From for Error { + #[inline] + fn from(x: NulError) -> Self { + Error::CStringConversion(x) + } +} diff --git a/vapoursynth/src/map/iterators.rs b/vapoursynth/src/map/iterators.rs new file mode 100644 index 0000000..c186095 --- /dev/null +++ b/vapoursynth/src/map/iterators.rs @@ -0,0 +1,118 @@ +use std::borrow::Cow; + +use super::*; + +/// An iterator over the keys of a map. +#[derive(Debug, Clone, Copy)] +pub struct Keys<'map, 'elem: 'map> { + map: &'map Map<'elem>, + count: usize, + index: usize, +} + +impl<'map, 'elem> Keys<'map, 'elem> { + #[inline] + pub(crate) fn new(map: &'map Map<'elem>) -> Self { + Self { + map, + count: map.key_count(), + index: 0, + } + } +} + +impl<'map, 'elem> Iterator for Keys<'map, 'elem> { + type Item = &'map str; + + #[inline] + fn next(&mut self) -> Option { + if self.index == self.count { + return None; + } + + let key = self.map.key(self.index); + self.index += 1; + Some(key) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.count - self.index; + (len, Some(len)) + } +} + +impl<'map, 'elem> ExactSizeIterator for Keys<'map, 'elem> {} + +/// An iterator over the values associated with a certain key of a map. +#[derive(Debug, Clone)] +pub struct ValueIter<'map, 'elem: 'map, T> { + map: &'map Map<'elem>, + key: CString, + count: i32, + index: i32, + _variance: PhantomData T>, +} + +macro_rules! impl_value_iter { + ($value_type:path, $type:ty, $func:ident) => { + impl<'map, 'elem> ValueIter<'map, 'elem, $type> { + /// Creates a `ValueIter` from the given `map` and `key`. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn new(map: &'map Map<'elem>, key: CString) -> Result { + // Check if the value type is correct. + match map.value_type_raw_unchecked(&key)? { + $value_type => {} + _ => return Err(Error::WrongValueType), + }; + + let count = map.value_count_raw_unchecked(&key)? as i32; + Ok(Self { + map, + key, + count, + index: 0, + _variance: PhantomData, + }) + } + } + + impl<'map, 'elem> Iterator for ValueIter<'map, 'elem, $type> { + type Item = $type; + + #[inline] + fn next(&mut self) -> Option { + if self.index == self.count { + return None; + } + + let value = unsafe { self.map.$func(&self.key, self.index).unwrap() }; + self.index += 1; + + Some(value) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = (self.count - self.index) as usize; + (len, Some(len)) + } + } + + impl<'map, 'elem> ExactSizeIterator for ValueIter<'map, 'elem, $type> {} + }; +} + +impl_value_iter!(ValueType::Int, i64, get_int_raw_unchecked); +impl_value_iter!(ValueType::Float, f64, get_float_raw_unchecked); +impl_value_iter!(ValueType::Data, &'map [u8], get_data_raw_unchecked); +impl_value_iter!(ValueType::Node, Node<'elem>, get_node_raw_unchecked); +impl_value_iter!(ValueType::Frame, FrameRef<'elem>, get_frame_raw_unchecked); +impl_value_iter!( + ValueType::Function, + Function<'elem>, + get_function_raw_unchecked +); diff --git a/vapoursynth/src/map/mod.rs b/vapoursynth/src/map/mod.rs new file mode 100644 index 0000000..c63b655 --- /dev/null +++ b/vapoursynth/src/map/mod.rs @@ -0,0 +1,1060 @@ +//! VapourSynth maps. + +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_char; +use std::ptr::{self, NonNull}; +use std::{mem, result, slice}; +use vapoursynth_sys as ffi; + +use api::API; +use frame::{Frame, FrameRef}; +use function::Function; +use node::Node; + +mod errors; +pub use self::errors::{Error, InvalidKeyError, Result}; + +mod iterators; +pub use self::iterators::{Keys, ValueIter}; + +mod value; +pub use self::value::{Value, ValueType}; + +/// A VapourSynth map. +/// +/// A map contains key-value pairs where the value is zero or more elements of a certain type. +// This type is intended to be publicly used only in reference form. +#[derive(Debug)] +pub struct Map<'elem> { + // The actual mutability of this depends on whether it's accessed via `&Map` or `&mut Map`. + handle: NonNull, + _elem: PhantomData<&'elem ()>, +} + +/// A reference to a VapourSynth map. +#[derive(Debug)] +pub struct MapRef<'owner, 'elem> { + // Only immutable references to this are allowed. + map: Map<'elem>, + _owner: PhantomData<&'owner ()>, +} + +/// A reference to a mutable VapourSynth map. +#[derive(Debug)] +pub struct MapRefMut<'owner, 'elem> { + // Both mutable and immutable references to this are allowed. + map: Map<'elem>, + _owner: PhantomData<&'owner ()>, +} + +/// An owned VapourSynth map. +#[derive(Debug)] +pub struct OwnedMap<'elem> { + // Both mutable and immutable references to this are allowed. + map: Map<'elem>, +} + +unsafe impl<'elem> Send for Map<'elem> {} +unsafe impl<'elem> Sync for Map<'elem> {} + +#[doc(hidden)] +impl<'elem> Deref for Map<'elem> { + type Target = ffi::VSMap; + + #[inline] + fn deref(&self) -> &Self::Target { + unsafe { self.handle.as_ref() } + } +} + +#[doc(hidden)] +impl<'elem> DerefMut for Map<'elem> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { self.handle.as_mut() } + } +} + +impl<'owner, 'elem> Deref for MapRef<'owner, 'elem> { + type Target = Map<'elem>; + + // Technically this should return `&'owner`. + #[inline] + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl<'owner, 'elem> Deref for MapRefMut<'owner, 'elem> { + type Target = Map<'elem>; + + // Technically this should return `&'owner`. + #[inline] + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl<'owner, 'elem> DerefMut for MapRefMut<'owner, 'elem> { + // Technically this should return `&'owner`. + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl<'elem> Drop for OwnedMap<'elem> { + #[inline] + fn drop(&mut self) { + unsafe { + API::get_cached().free_map(&mut self.map); + } + } +} + +impl<'elem> Deref for OwnedMap<'elem> { + type Target = Map<'elem>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl<'elem> DerefMut for OwnedMap<'elem> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl<'elem> OwnedMap<'elem> { + /// Creates a new map. + #[inline] + pub fn new(api: API) -> Self { + Self { + map: unsafe { Map::from_ptr(api.create_map()) }, + } + } + + /// Wraps pointer into `OwnedMap`. + /// + /// # Safety + /// The caller needs to ensure the pointer and the lifetime is valid and that this is an owned + /// map pointer. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSMap) -> Self { + Self { + map: Map::from_ptr(handle), + } + } +} + +impl<'owner, 'elem> MapRef<'owner, 'elem> { + /// Wraps pointer into `MapRef`. + /// + /// # Safety + /// The caller needs to ensure the pointer and the lifetimes are valid, and that there are no + /// mutable references to the given map. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *const ffi::VSMap) -> Self { + Self { + map: Map::from_ptr(handle), + _owner: PhantomData, + } + } +} + +impl<'owner, 'elem> MapRefMut<'owner, 'elem> { + /// Wraps pointer into `MapRefMut`. + /// + /// # Safety + /// The caller needs to ensure the pointer and the lifetimes are valid, and that there are no + /// references to the given map. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSMap) -> Self { + Self { + map: Map::from_ptr(handle), + _owner: PhantomData, + } + } +} + +/// Turns a `prop_get_something()` error into a `Result`. +#[inline] +fn handle_get_prop_error(error: i32) -> Result<()> { + if error == 0 { + Ok(()) + } else { + Err(match error { + x if x == ffi::VSGetPropErrors::peUnset as i32 => Error::KeyNotFound, + x if x == ffi::VSGetPropErrors::peType as i32 => Error::WrongValueType, + x if x == ffi::VSGetPropErrors::peIndex as i32 => Error::IndexOutOfBounds, + _ => unreachable!(), + }) + } +} + +/// Turns a `prop_set_something(paAppend)` error into a `Result`. +#[inline] +fn handle_append_prop_error(error: i32) -> Result<()> { + if error != 0 { + debug_assert!(error == 1); + Err(Error::WrongValueType) + } else { + Ok(()) + } +} + +impl<'elem> Map<'elem> { + /// Wraps pointer into `Map`. + /// + /// # Safety + /// The caller needs to ensure the pointer is valid, the element lifetime is valid, and that + /// the resulting `Map` gets put into `MapRef` or `MapRefMut` or `OwnedMap` correctly. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *const ffi::VSMap) -> Self { + Self { + handle: NonNull::new_unchecked(handle as *mut ffi::VSMap), + _elem: PhantomData, + } + } + + /// Checks if the key is valid. Valid keys start with an alphabetic character or an underscore, + /// and contain only alphanumeric characters and underscores. + pub fn is_key_valid(key: &str) -> result::Result<(), InvalidKeyError> { + if key.is_empty() { + return Err(InvalidKeyError::EmptyKey); + } + + let mut chars = key.chars(); + + let first = chars.next().unwrap(); + if !first.is_ascii_alphabetic() && first != '_' { + return Err(InvalidKeyError::InvalidCharacter(0)); + } + + for (i, c) in chars.enumerate() { + if !c.is_ascii_alphanumeric() && c != '_' { + return Err(InvalidKeyError::InvalidCharacter(i + 1)); + } + } + + Ok(()) + } + + /// Checks if the key is valid and makes it a `CString`. + #[inline] + pub(crate) fn make_raw_key(key: &str) -> Result { + Map::is_key_valid(key)?; + Ok(CString::new(key).unwrap()) + } + + /// Clears the map. + #[inline] + pub fn clear(&mut self) { + unsafe { + API::get_cached().clear_map(self); + } + } + + /// Returns the error message contained in the map, if any. + #[inline] + pub fn error(&self) -> Option> { + let error_message = unsafe { API::get_cached().get_error(self) }; + if error_message.is_null() { + return None; + } + + let error_message = unsafe { CStr::from_ptr(error_message) }; + Some(error_message.to_string_lossy()) + } + + /// Adds an error message to a map. The map is cleared first. + #[inline] + pub fn set_error(&mut self, error_message: &str) -> Result<()> { + let error_message = CString::new(error_message)?; + unsafe { + API::get_cached().set_error(self, error_message.as_ptr()); + } + Ok(()) + } + + /// Returns the number of keys contained in a map. + #[inline] + pub fn key_count(&self) -> usize { + let count = unsafe { API::get_cached().prop_num_keys(self) }; + debug_assert!(count >= 0); + count as usize + } + + /// Returns a key from a map. + /// + /// # Panics + /// Panics if `index >= self.key_count()`. + #[inline] + pub(crate) fn key_raw(&self, index: usize) -> &CStr { + assert!(index < self.key_count()); + let index = index as i32; + + unsafe { CStr::from_ptr(API::get_cached().prop_get_key(self, index)) } + } + + /// Returns a key from a map. + /// + /// # Panics + /// Panics if `index >= self.key_count()`. + #[inline] + pub fn key(&self, index: usize) -> &str { + self.key_raw(index).to_str().unwrap() + } + + /// Returns an iterator over all keys in a map. + #[inline] + pub fn keys(&self) -> Keys { + Keys::new(self) + } + + /// Returns the number of elements associated with a key in a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn value_count_raw_unchecked(&self, key: &CStr) -> Result { + let rv = API::get_cached().prop_num_elements(self, key.as_ptr()); + if rv == -1 { + Err(Error::KeyNotFound) + } else { + debug_assert!(rv >= 0); + Ok(rv as usize) + } + } + + /// Returns the number of elements associated with a key in a map. + #[inline] + pub fn value_count(&self, key: &str) -> Result { + let key = Map::make_raw_key(key)?; + unsafe { self.value_count_raw_unchecked(&key) } + } + + /// Retrieves a value type from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn value_type_raw_unchecked(&self, key: &CStr) -> Result { + match API::get_cached().prop_get_type(self, key.as_ptr()) { + x if x == ffi::VSPropTypes::ptUnset as c_char => Err(Error::KeyNotFound), + x if x == ffi::VSPropTypes::ptInt as c_char => Ok(ValueType::Int), + x if x == ffi::VSPropTypes::ptFloat as c_char => Ok(ValueType::Float), + x if x == ffi::VSPropTypes::ptData as c_char => Ok(ValueType::Data), + x if x == ffi::VSPropTypes::ptNode as c_char => Ok(ValueType::Node), + x if x == ffi::VSPropTypes::ptFrame as c_char => Ok(ValueType::Frame), + x if x == ffi::VSPropTypes::ptFunction as c_char => Ok(ValueType::Function), + _ => unreachable!(), + } + } + + /// Retrieves a value type from a map. + #[inline] + pub fn value_type(&self, key: &str) -> Result { + let key = Map::make_raw_key(key)?; + unsafe { self.value_type_raw_unchecked(&key) } + } + + /// Deletes the given key. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn delete_key_raw_unchecked(&mut self, key: &CStr) -> Result<()> { + let result = API::get_cached().prop_delete_key(self, key.as_ptr()); + if result == 0 { + Err(Error::KeyNotFound) + } else { + debug_assert!(result == 1); + Ok(()) + } + } + + /// Deletes the given key. + #[inline] + pub fn delete_key(&mut self, key: &str) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.delete_key_raw_unchecked(&key) } + } + + /// Touches the key. That is, if the key exists, nothing happens, otherwise a key is created + /// with no values associated. + /// + /// # Safety + /// The caller must ensure `key` is valid. + pub(crate) unsafe fn touch_raw_unchecked(&mut self, key: &CStr, value_type: ValueType) { + #[cfg_attr(rustfmt, rustfmt_skip)] + macro_rules! touch_value { + ($func:ident, $value:expr) => {{ + let result = + API::get_cached().$func( + self, + key.as_ptr(), + $value, + ffi::VSPropAppendMode::paTouch + ); + debug_assert!(result == 0); + }}; + } + + match value_type { + ValueType::Int => touch_value!(prop_set_int, 0), + ValueType::Float => touch_value!(prop_set_float, 0f64), + ValueType::Data => touch_value!(prop_set_data, &[]), + ValueType::Node => touch_value!(prop_set_node, ptr::null_mut()), + ValueType::Frame => touch_value!(prop_set_frame, ptr::null()), + ValueType::Function => touch_value!(prop_set_func, ptr::null_mut()), + } + } + + /// Touches the key. That is, if the key exists, nothing happens, otherwise a key is created + /// with no values associated. + #[inline] + pub fn touch(&mut self, key: &str, value_type: ValueType) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.touch_raw_unchecked(&key, value_type); + } + Ok(()) + } + + /// Retrieves a property value. + #[inline] + pub fn get<'map, T: Value<'map, 'elem>>(&'map self, key: &str) -> Result { + T::get_from_map(self, key) + } + + /// Retrieves an iterator over the map values. + #[inline] + pub fn get_iter<'map, T: Value<'map, 'elem>>( + &'map self, + key: &str, + ) -> Result> { + T::get_iter_from_map(self, key) + } + + /// Sets a property value. + #[inline] + pub fn set<'map, T: Value<'map, 'elem>>(&'map mut self, key: &str, x: &T) -> Result<()> { + T::store_in_map(self, key, x) + } + + /// Appends a property value. + #[inline] + pub fn append<'map, T: Value<'map, 'elem>>(&'map mut self, key: &str, x: &T) -> Result<()> { + T::append_to_map(self, key, x) + } + + /// Retrieves an integer from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_int(&self, key: &str) -> Result { + let key = Map::make_raw_key(key)?; + unsafe { self.get_int_raw_unchecked(&key, 0) } + } + + /// Retrieves integers from a map. + #[inline] + pub fn get_int_iter<'map>(&'map self, key: &str) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::::new(self, key) } + } + + /// Retrieves an array of integers from a map. + /// + /// This is faster than iterating over a `get_int_iter()`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub fn get_int_array(&self, key: &str) -> Result<&[i64]> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_int_array_raw_unchecked(&key) } + } + + /// Retrieves a floating point number from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_float(&self, key: &str) -> Result { + let key = Map::make_raw_key(key)?; + unsafe { self.get_float_raw_unchecked(&key, 0) } + } + + /// Retrieves an array of floating point numbers from a map. + /// + /// This is faster than iterating over a `get_float_iter()`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub fn get_float_array(&self, key: &str) -> Result<&[f64]> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_float_array_raw_unchecked(&key) } + } + + /// Retrieves floating point numbers from a map. + #[inline] + pub fn get_float_iter<'map>(&'map self, key: &str) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::::new(self, key) } + } + + /// Retrieves data from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_data(&self, key: &str) -> Result<&[u8]> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_data_raw_unchecked(&key, 0) } + } + + /// Retrieves data from a map. + #[inline] + pub fn get_data_iter<'map>( + &'map self, + key: &str, + ) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::<&[u8]>::new(self, key) } + } + + /// Retrieves a node from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_node(&self, key: &str) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_node_raw_unchecked(&key, 0) } + } + + /// Retrieves nodes from a map. + #[inline] + pub fn get_node_iter<'map>( + &'map self, + key: &str, + ) -> Result>> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::::new(self, key) } + } + + /// Retrieves a frame from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_frame(&self, key: &str) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_frame_raw_unchecked(&key, 0) } + } + + /// Retrieves frames from a map. + #[inline] + pub fn get_frame_iter<'map>( + &'map self, + key: &str, + ) -> Result>> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::::new(self, key) } + } + + /// Retrieves a function from a map. + /// + /// This function retrieves the first value associated with the key. + #[inline] + pub fn get_function(&self, key: &str) -> Result> { + let key = Map::make_raw_key(key)?; + unsafe { self.get_function_raw_unchecked(&key, 0) } + } + + /// Retrieves functions from a map. + #[inline] + pub fn get_function_iter<'map>( + &'map self, + key: &str, + ) -> Result>> { + let key = Map::make_raw_key(key)?; + unsafe { ValueIter::::new(self, key) } + } + + /// Retrieves an integer from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_int_raw_unchecked(&self, key: &CStr, index: i32) -> Result { + let mut error = 0; + let value = API::get_cached().prop_get_int(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + Ok(value) + } + + /// Retrieves an array of integers from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn get_int_array_raw_unchecked(&self, key: &CStr) -> Result<&[i64]> { + let mut error = 0; + let value = API::get_cached().prop_get_int_array(self, key.as_ptr(), &mut error); + handle_get_prop_error(error)?; + + let length = self.value_count_raw_unchecked(key).unwrap(); + Ok(slice::from_raw_parts(value, length)) + } + + /// Retrieves a floating point number from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_float_raw_unchecked(&self, key: &CStr, index: i32) -> Result { + let mut error = 0; + let value = API::get_cached().prop_get_float(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + Ok(value) + } + + /// Retrieves an array of floating point numbers from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn get_float_array_raw_unchecked(&self, key: &CStr) -> Result<&[f64]> { + let mut error = 0; + let value = API::get_cached().prop_get_float_array(self, key.as_ptr(), &mut error); + handle_get_prop_error(error)?; + + let length = self.value_count_raw_unchecked(key).unwrap(); + Ok(slice::from_raw_parts(value, length)) + } + + /// Retrieves data from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_data_raw_unchecked(&self, key: &CStr, index: i32) -> Result<&[u8]> { + let mut error = 0; + let value = API::get_cached().prop_get_data(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + let mut error = 0; + let length = API::get_cached().prop_get_data_size(self, key.as_ptr(), index, &mut error); + debug_assert!(error == 0); + debug_assert!(length >= 0); + + Ok(slice::from_raw_parts(value as *const u8, length as usize)) + } + + /// Retrieves a node from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_node_raw_unchecked( + &self, + key: &CStr, + index: i32, + ) -> Result> { + let mut error = 0; + let value = API::get_cached().prop_get_node(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + Ok(Node::from_ptr(value)) + } + + /// Retrieves a frame from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_frame_raw_unchecked( + &self, + key: &CStr, + index: i32, + ) -> Result> { + let mut error = 0; + let value = API::get_cached().prop_get_frame(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + Ok(FrameRef::from_ptr(value)) + } + + /// Retrieves a function from a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn get_function_raw_unchecked( + &self, + key: &CStr, + index: i32, + ) -> Result> { + let mut error = 0; + let value = API::get_cached().prop_get_func(self, key.as_ptr(), index, &mut error); + handle_get_prop_error(error)?; + + Ok(Function::from_ptr(value)) + } + + /// Appends an integer to a map. + #[inline] + pub fn append_int(&mut self, key: &str, x: i64) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_int_raw_unchecked(&key, x) } + } + + /// Appends a floating point number to a map. + #[inline] + pub fn append_float(&mut self, key: &str, x: f64) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_float_raw_unchecked(&key, x) } + } + + /// Appends data to a map. + #[inline] + pub fn append_data(&mut self, key: &str, x: &[u8]) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_data_raw_unchecked(&key, x) } + } + + /// Appends a node to a map. + #[inline] + pub fn append_node(&mut self, key: &str, x: &Node<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_node_raw_unchecked(&key, x) } + } + + /// Appends a frame to a map. + #[inline] + pub fn append_frame(&mut self, key: &str, x: &Frame<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_frame_raw_unchecked(&key, x) } + } + + /// Appends a function to a map. + #[inline] + pub fn append_function(&mut self, key: &str, x: &Function<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { self.append_function_raw_unchecked(&key, x) } + } + + /// Appends an integer to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_int_raw_unchecked(&mut self, key: &CStr, x: i64) -> Result<()> { + let error = + API::get_cached().prop_set_int(self, key.as_ptr(), x, ffi::VSPropAppendMode::paAppend); + + handle_append_prop_error(error) + } + + /// Appends a floating point number to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_float_raw_unchecked(&mut self, key: &CStr, x: f64) -> Result<()> { + let error = API::get_cached().prop_set_float( + self, + key.as_ptr(), + x, + ffi::VSPropAppendMode::paAppend, + ); + + handle_append_prop_error(error) + } + + /// Appends data to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_data_raw_unchecked(&mut self, key: &CStr, x: &[u8]) -> Result<()> { + let error = + API::get_cached().prop_set_data(self, key.as_ptr(), x, ffi::VSPropAppendMode::paAppend); + + handle_append_prop_error(error) + } + + /// Appends a node to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_node_raw_unchecked( + &mut self, + key: &CStr, + x: &Node<'elem>, + ) -> Result<()> { + let error = API::get_cached().prop_set_node( + self, + key.as_ptr(), + x.ptr(), + ffi::VSPropAppendMode::paAppend, + ); + + handle_append_prop_error(error) + } + + /// Appends a frame to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_frame_raw_unchecked( + &mut self, + key: &CStr, + x: &Frame<'elem>, + ) -> Result<()> { + let error = API::get_cached().prop_set_frame( + self, + key.as_ptr(), + x.deref(), + ffi::VSPropAppendMode::paAppend, + ); + + handle_append_prop_error(error) + } + + /// Appends a function to a map. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn append_function_raw_unchecked( + &mut self, + key: &CStr, + x: &Function<'elem>, + ) -> Result<()> { + let error = API::get_cached().prop_set_func( + self, + key.as_ptr(), + x.ptr(), + ffi::VSPropAppendMode::paAppend, + ); + + handle_append_prop_error(error) + } + + /// Sets a property value to an integer. + #[inline] + pub fn set_int(&mut self, key: &str, x: i64) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_int_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to an integer array. + /// + /// This is faster than calling `append_int()` in a loop. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub fn set_int_array(&mut self, key: &str, x: &[i64]) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_int_array_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to a floating point number. + #[inline] + pub fn set_float(&mut self, key: &str, x: f64) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_float_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to a floating point number array. + /// + /// This is faster than calling `append_float()` in a loop. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub fn set_float_array(&mut self, key: &str, x: &[f64]) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_float_array_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to data. + #[inline] + pub fn set_data(&mut self, key: &str, x: &[u8]) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_data_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to a node. + #[inline] + pub fn set_node(&mut self, key: &str, x: &Node<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_node_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to a frame. + #[inline] + pub fn set_frame(&mut self, key: &str, x: &Frame<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_frame_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to a function. + #[inline] + pub fn set_function(&mut self, key: &str, x: &Function<'elem>) -> Result<()> { + let key = Map::make_raw_key(key)?; + unsafe { + self.set_function_raw_unchecked(&key, x); + } + Ok(()) + } + + /// Sets a property value to an integer. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_int_raw_unchecked(&mut self, key: &CStr, x: i64) { + let error = + API::get_cached().prop_set_int(self, key.as_ptr(), x, ffi::VSPropAppendMode::paReplace); + + debug_assert!(error == 0); + } + + /// Sets a property value to an integer array. + /// + /// # Safety + /// The caller must ensure `key` is valid. + /// + /// # Panics + /// Panics if `x.len()` can't fit in an `i32`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn set_int_array_raw_unchecked(&mut self, key: &CStr, x: &[i64]) { + let error = API::get_cached().prop_set_int_array(self, key.as_ptr(), x); + + debug_assert!(error == 0); + } + + /// Sets a property value to a floating point number. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_float_raw_unchecked(&mut self, key: &CStr, x: f64) { + let error = API::get_cached().prop_set_float( + self, + key.as_ptr(), + x, + ffi::VSPropAppendMode::paReplace, + ); + + debug_assert!(error == 0); + } + + /// Sets a property value to a floating point number array. + /// + /// # Safety + /// The caller must ensure `key` is valid. + /// + /// # Panics + /// Panics if `x.len()` can't fit in an `i32`. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub(crate) unsafe fn set_float_array_raw_unchecked(&mut self, key: &CStr, x: &[f64]) { + let error = API::get_cached().prop_set_float_array(self, key.as_ptr(), x); + + debug_assert!(error == 0); + } + + /// Sets a property value to data. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_data_raw_unchecked(&mut self, key: &CStr, x: &[u8]) { + let error = API::get_cached().prop_set_data( + self, + key.as_ptr(), + x, + ffi::VSPropAppendMode::paReplace, + ); + + debug_assert!(error == 0); + } + + /// Sets a property value to a node. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_node_raw_unchecked(&mut self, key: &CStr, x: &Node<'elem>) { + let error = API::get_cached().prop_set_node( + self, + key.as_ptr(), + x.ptr(), + ffi::VSPropAppendMode::paReplace, + ); + + debug_assert!(error == 0); + } + + /// Sets a property value to a frame. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_frame_raw_unchecked(&mut self, key: &CStr, x: &Frame<'elem>) { + let error = API::get_cached().prop_set_frame( + self, + key.as_ptr(), + x.deref(), + ffi::VSPropAppendMode::paReplace, + ); + + debug_assert!(error == 0); + } + + /// Sets a property value to a function. + /// + /// # Safety + /// The caller must ensure `key` is valid. + #[inline] + pub(crate) unsafe fn set_function_raw_unchecked(&mut self, key: &CStr, x: &Function<'elem>) { + let error = API::get_cached().prop_set_func( + self, + key.as_ptr(), + x.ptr(), + ffi::VSPropAppendMode::paReplace, + ); + + debug_assert!(error == 0); + } +} diff --git a/vapoursynth/src/map/value.rs b/vapoursynth/src/map/value.rs new file mode 100644 index 0000000..6fb2bc1 --- /dev/null +++ b/vapoursynth/src/map/value.rs @@ -0,0 +1,161 @@ +use frame::FrameRef; +use function::Function; +use map::{Map, Result, ValueIter}; +use node::Node; + +/// An enumeration of all possible value types. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum ValueType { + Int, + Float, + Data, + Node, + Frame, + Function, +} + +/// A trait for values which can be stored in a map. +pub trait Value<'map, 'elem: 'map>: Sized { + /// Retrieves the value from the map. + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Result; + + /// Retrieves an iterator over the values from the map. + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result>; + + /// Sets the property value in the map. + fn store_in_map(map: &'map mut Map<'elem>, key: &str, x: &Self) -> Result<()>; + + /// Appends the value to the map. + fn append_to_map(map: &'map mut Map<'elem>, key: &str, x: &Self) -> Result<()>; +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for i64 { + #[inline] + fn get_from_map(map: &Map, key: &str) -> Result { + map.get_int(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_int_iter(key) + } + + #[inline] + fn store_in_map(map: &mut Map, key: &str, x: &Self) -> Result<()> { + map.set_int(key, *x) + } + + #[inline] + fn append_to_map(map: &mut Map, key: &str, x: &Self) -> Result<()> { + map.append_int(key, *x) + } +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for f64 { + fn get_from_map(map: &Map, key: &str) -> Result { + map.get_float(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_float_iter(key) + } + + #[inline] + fn store_in_map(map: &mut Map, key: &str, x: &Self) -> Result<()> { + map.set_float(key, *x) + } + + #[inline] + fn append_to_map(map: &mut Map, key: &str, x: &Self) -> Result<()> { + map.append_float(key, *x) + } +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for &'map [u8] { + #[inline] + fn get_from_map(map: &'map Map, key: &str) -> Result { + map.get_data(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_data_iter(key) + } + + #[inline] + fn store_in_map(map: &'map mut Map, key: &str, x: &Self) -> Result<()> { + map.set_data(key, x) + } + + #[inline] + fn append_to_map(map: &'map mut Map, key: &str, x: &Self) -> Result<()> { + map.append_data(key, x) + } +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for Node<'elem> { + #[inline] + fn get_from_map(map: &Map<'elem>, key: &str) -> Result { + map.get_node(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_node_iter(key) + } + + #[inline] + fn store_in_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.set_node(key, x) + } + + #[inline] + fn append_to_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.append_node(key, x) + } +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for FrameRef<'elem> { + #[inline] + fn get_from_map(map: &Map<'elem>, key: &str) -> Result { + map.get_frame(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_frame_iter(key) + } + + #[inline] + fn store_in_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.set_frame(key, x) + } + + #[inline] + fn append_to_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.append_frame(key, x) + } +} + +impl<'map, 'elem: 'map> Value<'map, 'elem> for Function<'elem> { + #[inline] + fn get_from_map(map: &Map<'elem>, key: &str) -> Result { + map.get_function(key) + } + + #[inline] + fn get_iter_from_map(map: &'map Map<'elem>, key: &str) -> Result> { + map.get_function_iter(key) + } + + #[inline] + fn store_in_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.set_function(key, x) + } + + #[inline] + fn append_to_map(map: &mut Map<'elem>, key: &str, x: &Self) -> Result<()> { + map.append_function(key, x) + } +} diff --git a/vapoursynth/src/node/errors.rs b/vapoursynth/src/node/errors.rs new file mode 100644 index 0000000..8106db9 --- /dev/null +++ b/vapoursynth/src/node/errors.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; +use std::error::Error; +use std::ffi::CStr; +use std::fmt; + +/// A container for a `get_frame` error. +#[derive(Debug)] +pub struct GetFrameError<'a>(Cow<'a, CStr>); + +impl<'a> fmt::Display for GetFrameError<'a> { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_string_lossy()) + } +} + +impl<'a> Error for GetFrameError<'a> { + #[inline] + fn description(&self) -> &str { + "VapourSynth error" + } +} + +impl<'a> GetFrameError<'a> { + /// Creates a new `GetFrameError` with the given error message. + #[inline] + pub(crate) fn new(message: Cow<'a, CStr>) -> Self { + GetFrameError(message) + } + + /// Consumes this error, returning its underlying error message. + #[inline] + pub fn into_inner(self) -> Cow<'a, CStr> { + self.0 + } +} diff --git a/vapoursynth/src/node/mod.rs b/vapoursynth/src/node/mod.rs new file mode 100644 index 0000000..7037c0a --- /dev/null +++ b/vapoursynth/src/node/mod.rs @@ -0,0 +1,280 @@ +//! VapourSynth nodes. + +use std::borrow::Cow; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::os::raw::{c_char, c_void}; +use std::process; +use std::ptr::NonNull; +use std::{mem, panic}; +use vapoursynth_sys as ffi; + +use api::API; +use frame::FrameRef; +use plugins::FrameContext; +use video_info::VideoInfo; + +mod errors; +pub use self::errors::GetFrameError; + +bitflags! { + /// Node flags. + pub struct Flags: i32 { + /// This flag indicates that the frames returned by the filter should not be cached. "Fast" + /// filters should set this to reduce cache bloat. + const NO_CACHE = ffi::VSNodeFlags_nfNoCache.0; + /// This flag must not be used in third-party filters. It is used to mark instances of the + /// built-in Cache filter. Strange things may happen to your filter if you use this flag. + const IS_CACHE = ffi::VSNodeFlags_nfIsCache.0; + + /// This flag should be used by filters which prefer linear access, like source filters, + /// where seeking around can cause significant slowdowns. This flag only has any effect if + /// the filter using it is immediately followed by an instance of the built-in Cache + /// filter. + #[cfg(feature = "gte-vapoursynth-api-33")] + const MAKE_LINEAR = ffi::VSNodeFlags_nfMakeLinear.0; + } +} + +impl From for Flags { + #[inline] + fn from(flags: ffi::VSNodeFlags) -> Self { + Self::from_bits_truncate(flags.0) + } +} + +/// A reference to a node in the constructed filter graph. +#[derive(Debug)] +pub struct Node<'core> { + handle: NonNull, + _owner: PhantomData<&'core ()>, +} + +unsafe impl<'core> Send for Node<'core> {} +unsafe impl<'core> Sync for Node<'core> {} + +impl<'core> Drop for Node<'core> { + #[inline] + fn drop(&mut self) { + unsafe { + API::get_cached().free_node(self.handle.as_ptr()); + } + } +} + +impl<'core> Clone for Node<'core> { + #[inline] + fn clone(&self) -> Self { + let handle = unsafe { API::get_cached().clone_node(self.handle.as_ptr()) }; + Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + _owner: PhantomData, + } + } +} + +impl<'core> Node<'core> { + /// Wraps `handle` in a `Node`. + /// + /// # Safety + /// The caller must ensure `handle` and the lifetime is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSNodeRef) -> Self { + Self { + handle: NonNull::new_unchecked(handle), + _owner: PhantomData, + } + } + + /// Returns the underlying pointer. + #[inline] + pub(crate) fn ptr(&self) -> *mut ffi::VSNodeRef { + self.handle.as_ptr() + } + + /// Returns the video info associated with this `Node`. + // Since we don't store the pointer to the actual `ffi::VSVideoInfo` and the lifetime is that + // of the `ffi::VSFormat`, this returns `VideoInfo<'core>` rather than `VideoInfo<'a>`. + #[inline] + pub fn info(&self) -> VideoInfo<'core> { + unsafe { + let ptr = API::get_cached().get_video_info(self.handle.as_ptr()); + VideoInfo::from_ptr(ptr) + } + } + + /// Generates a frame directly. + /// + /// The `'error` lifetime is unbounded because this function always returns owned data. + /// + /// # Panics + /// Panics is `n` is greater than `i32::max_value()`. + pub fn get_frame<'error>(&self, n: usize) -> Result, GetFrameError<'error>> { + assert!(n <= i32::max_value() as usize); + let n = n as i32; + + // Kinda arbitrary. Same value as used in vsvfw. + const ERROR_BUF_CAPACITY: usize = 32 * 1024; + + let mut err_buf = Vec::with_capacity(ERROR_BUF_CAPACITY); + err_buf.resize(ERROR_BUF_CAPACITY, 0); + let mut err_buf = err_buf.into_boxed_slice(); + + let handle = unsafe { API::get_cached().get_frame(n, self.handle.as_ptr(), &mut *err_buf) }; + + if handle.is_null() { + // TODO: remove this extra allocation by reusing `Box<[c_char]>`. + let error = unsafe { CStr::from_ptr(err_buf.as_ptr()) }.to_owned(); + Err(GetFrameError::new(Cow::Owned(error))) + } else { + Ok(unsafe { FrameRef::from_ptr(handle) }) + } + } + + /// Requests the generation of a frame. When the frame is ready, a user-provided function is + /// called. + /// + /// If multiple frames were requested, they can be returned in any order. + /// + /// The callback arguments are: + /// + /// - the generated frame or an error message if the generation failed, + /// - the frame number (equal to `n`), + /// - the node that generated the frame (the same as `self`). + /// + /// If the callback panics, the process is aborted. + /// + /// # Panics + /// Panics is `n` is greater than `i32::max_value()`. + pub fn get_frame_async(&self, n: usize, callback: F) + where + F: FnOnce(Result, GetFrameError>, usize, Node<'core>) + Send + 'core, + { + struct CallbackData<'core> { + callback: Box + 'core>, + } + + // A little bit of magic for Box. + trait CallbackFn<'core> { + fn call( + self: Box, + frame: Result, GetFrameError>, + n: usize, + node: Node<'core>, + ); + } + + impl<'core, F> CallbackFn<'core> for F + where + F: FnOnce(Result, GetFrameError>, usize, Node<'core>), + { + #[cfg_attr(feature = "cargo-clippy", allow(boxed_local))] + fn call( + self: Box, + frame: Result, GetFrameError>, + n: usize, + node: Node<'core>, + ) { + (self)(frame, n, node) + } + } + + unsafe extern "system" fn c_callback( + user_data: *mut c_void, + frame: *const ffi::VSFrameRef, + n: i32, + node: *mut ffi::VSNodeRef, + error_msg: *const c_char, + ) { + // The actual lifetime isn't 'static, it's 'core, but we don't really have a way of + // retrieving it. + let user_data = Box::from_raw(user_data as *mut CallbackData<'static>); + + let closure = panic::AssertUnwindSafe(move || { + let frame = if frame.is_null() { + debug_assert!(!error_msg.is_null()); + let error_msg = Cow::Borrowed(CStr::from_ptr(error_msg)); + Err(GetFrameError::new(error_msg)) + } else { + debug_assert!(error_msg.is_null()); + Ok(FrameRef::from_ptr(frame)) + }; + + let node = Node::from_ptr(node); + + debug_assert!(n >= 0); + let n = n as usize; + + user_data.callback.call(frame, n, node); + }); + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } + } + + assert!(n <= i32::max_value() as usize); + let n = n as i32; + + let user_data = Box::new(CallbackData { + callback: Box::new(callback), + }); + + let new_node = self.clone(); + + unsafe { + API::get_cached().get_frame_async( + n, + new_node.handle.as_ptr(), + Some(c_callback), + Box::into_raw(user_data) as *mut c_void, + ); + } + + // It'll be dropped by the callback. + mem::forget(new_node); + } + + /// Requests a frame from a node and returns immediately. + /// + /// This is only used in filters' "get frame" functions. + /// + /// A filter usually calls this function from `get_frame_initial()`. The requested frame can + /// then be retrieved using `get_frame_filter()` from within filter's `get_frame()` function. + /// + /// It is safe to request a frame more than once. An unimportant consequence of requesting a + /// frame more than once is that the filter's `get_frame()` function may be called more than + /// once for the same frame. + /// + /// It is best to request frames in ascending order, i.e. `n`, `n+1`, `n+2`, etc. + /// + /// # Panics + /// Panics is `n` is greater than `i32::max_value()`. + pub fn request_frame_filter(&self, context: FrameContext, n: usize) { + assert!(n <= i32::max_value() as usize); + let n = n as i32; + + unsafe { + API::get_cached().request_frame_filter(n, self.ptr(), context.ptr()); + } + } + + /// Retrieves a frame that was previously requested with `request_frame_filter()`. + /// + /// A filter usually calls this function from `get_frame()`. It is safe to retrieve a frame + /// more than once. + /// + /// # Panics + /// Panics is `n` is greater than `i32::max_value()`. + pub fn get_frame_filter(&self, context: FrameContext, n: usize) -> Option> { + assert!(n <= i32::max_value() as usize); + let n = n as i32; + + let ptr = unsafe { API::get_cached().get_frame_filter(n, self.ptr(), context.ptr()) }; + if ptr.is_null() { + None + } else { + Some(unsafe { FrameRef::from_ptr(ptr) }) + } + } +} diff --git a/vapoursynth/src/plugin.rs b/vapoursynth/src/plugin.rs new file mode 100644 index 0000000..9a1a727 --- /dev/null +++ b/vapoursynth/src/plugin.rs @@ -0,0 +1,112 @@ +//! VapourSynth plugins. + +use std::ffi::{CStr, CString, NulError}; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::NonNull; +use vapoursynth_sys as ffi; + +use api::API; +use map::{Map, OwnedMap}; +use plugins::{self, FilterFunction}; + +/// A VapourSynth plugin. +#[derive(Debug, Clone, Copy)] +pub struct Plugin<'core> { + handle: NonNull, + _owner: PhantomData<&'core ()>, +} + +unsafe impl<'core> Send for Plugin<'core> {} +unsafe impl<'core> Sync for Plugin<'core> {} + +impl<'core> Plugin<'core> { + /// Wraps `handle` in a `Plugin`. + /// + /// # Safety + /// The caller must ensure `handle` is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSPlugin) -> Self { + Self { + handle: NonNull::new_unchecked(handle), + _owner: PhantomData, + } + } + + /// Returns a map containing a list of the filters exported by a plugin. + /// + /// Keys: the filter names; + /// + /// Values: the filter name followed by its argument string, separated by a semicolon. + // TODO: parse the values on the crate side and return a nice struct. + #[inline] + pub fn functions(&self) -> OwnedMap<'core> { + unsafe { OwnedMap::from_ptr(API::get_cached().get_functions(self.handle.as_ptr())) } + } + + /// Returns the absolute path to the plugin, including the plugin's file name. This is the real + /// location of the plugin, i.e. there are no symbolic links in the path. + /// + /// Path elements are always delimited with forward slashes. + #[cfg(feature = "gte-vapoursynth-api-31")] + #[inline] + pub fn path(&self) -> Option<&'core CStr> { + let ptr = unsafe { API::get_cached().get_plugin_path(self.handle.as_ptr()) }; + if ptr.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(ptr) }) + } + } + + /// Invokes a filter. + /// + /// `invoke()` makes sure the filter has no compat input nodes, checks that the args passed to + /// the filter are consistent with the argument list registered by the plugin that contains the + /// filter, creates the filter, and checks that the filter doesn't return any compat nodes. If + /// everything goes smoothly, the filter will be ready to generate frames after `invoke()` + /// returns. + /// + /// Returns a map containing the filter's return value(s). Use `Map::error()` to check if the + /// filter was invoked successfully. + /// + /// Most filters will either add an error to the map, or one or more clips with the key `clip`. + /// The exception to this are functions, for example `LoadPlugin`, which doesn't return any + /// clips for obvious reasons. + #[inline] + pub fn invoke(&self, name: &str, args: &Map<'core>) -> Result, NulError> { + let name = CString::new(name)?; + Ok(unsafe { + OwnedMap::from_ptr(API::get_cached().invoke( + self.handle.as_ptr(), + name.as_ptr(), + args.deref(), + )) + }) + } + + /// Registers a filter function to be exported by a non-readonly plugin. + #[inline] + pub fn register_function(&self, filter_function: F) -> Result<(), NulError> { + // TODO: this is almost the same code as plugins::ffi::call_register_function(). + let name_cstring = CString::new(filter_function.name())?; + let args_cstring = CString::new(filter_function.args())?; + + let data = Box::new(plugins::ffi::FilterFunctionData:: { + filter_function, + name: name_cstring, + }); + + unsafe { + API::get_cached().register_function( + data.name.as_ptr(), + args_cstring.as_ptr(), + plugins::ffi::create::, + Box::into_raw(data) as _, + self.handle.as_ptr(), + ); + } + + Ok(()) + } +} diff --git a/vapoursynth/src/plugins/ffi.rs b/vapoursynth/src/plugins/ffi.rs new file mode 100644 index 0000000..21a64ff --- /dev/null +++ b/vapoursynth/src/plugins/ffi.rs @@ -0,0 +1,346 @@ +//! Internal stuff for plugin FFI handling. +use failure::Error; +use std::ffi::CString; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +use std::ptr::{self, NonNull}; +use std::{mem, panic, process}; +use vapoursynth_sys as ffi; + +use api::API; +use core::CoreRef; +use frame::FrameRef; +use map::{Map, MapRef, MapRefMut}; +use plugins::{Filter, FilterFunction, FrameContext, Metadata}; +use video_info::VideoInfo; + +/// Container for the internal filter function data. +pub(crate) struct FilterFunctionData { + pub filter_function: F, + // Store the name since it's supposed to be the same between two invocations (register and + // create_filter). + pub name: CString, +} + +/// Pushes the error backtrace into the given string. +fn push_backtrace(buf: &mut String, err: &Error) { + for cause in err.causes().skip(1) { + buf.push_str(&format!("Caused by: {}", cause)); + } + + buf.push_str(&format!("{}", err.backtrace())); +} + +/// Sets the video info of the output node of this filter. +unsafe extern "system" fn init( + _in_: *mut ffi::VSMap, + out: *mut ffi::VSMap, + instance_data: *mut *mut c_void, + node: *mut ffi::VSNode, + core: *mut ffi::VSCore, + _vsapi: *const ffi::VSAPI, +) { + let closure = move || { + let core = CoreRef::from_ptr(core); + // The actual lifetime isn't 'static, it's 'core, but we don't really have a way of + // retrieving it. + let filter = Box::from_raw(*(instance_data as *mut *mut Box + 'static>)); + + let vi = filter + .video_info(API::get_cached(), core) + .into_iter() + .map(VideoInfo::ffi_type) + .collect::>(); + API::get_cached().set_video_info(&vi, node); + + mem::forget(filter); + }; + + if panic::catch_unwind(closure).is_err() { + let closure = move || { + // We have to leak filter here because we can't guarantee that it's in a consistent + // state after a panic. + // + // Just set the error message. + let mut out = MapRefMut::from_ptr(out); + out.set_error("Panic during Filter::video_info()"); + }; + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } + } +} + +/// Drops the filter. +unsafe extern "system" fn free( + instance_data: *mut c_void, + core: *mut ffi::VSCore, + _vsapi: *const ffi::VSAPI, +) { + let closure = move || { + // The actual lifetime isn't 'static, it's 'core, but we don't really have a way of + // retrieving it. + let filter = Box::from_raw(instance_data as *mut Box + 'static>); + drop(filter); + }; + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } +} + +/// Calls `Filter::get_frame_initial()` and `Filter::get_frame()`. +unsafe extern "system" fn get_frame( + n: i32, + activation_reason: i32, + instance_data: *mut *mut c_void, + _frame_data: *mut *mut c_void, + frame_ctx: *mut ffi::VSFrameContext, + core: *mut ffi::VSCore, + _vsapi: *const ffi::VSAPI, +) -> *const ffi::VSFrameRef { + let closure = move || { + let api = API::get_cached(); + let core = CoreRef::from_ptr(core); + let context = FrameContext::from_ptr(frame_ctx); + + // The actual lifetime isn't 'static, it's 'core, but we don't really have a way of + // retrieving it. + let filter = Box::from_raw(*(instance_data as *mut *mut Box + 'static>)); + + debug_assert!(n >= 0); + let n = n as usize; + + let rv = match activation_reason { + x if x == ffi::VSActivationReason::arInitial as _ => { + match filter.get_frame_initial(api, core, context, n) { + Ok(Some(frame)) => { + let ptr = frame.deref().deref() as *const _; + // The ownership is transferred to the caller. + mem::forget(frame); + ptr + } + Ok(None) => ptr::null(), + Err(err) => { + let mut buf = String::new(); + + buf += &format!("Error in Filter::get_frame_initial(): {}", err.cause()); + + push_backtrace(&mut buf, &err); + + let buf = CString::new(buf.replace('\0', "\\0")).unwrap(); + api.set_filter_error(buf.as_ptr(), frame_ctx); + + ptr::null() + } + } + } + x if x == ffi::VSActivationReason::arAllFramesReady as _ => { + match filter.get_frame(api, core, context, n) { + Ok(frame) => { + let ptr = frame.deref().deref() as *const _; + // The ownership is transferred to the caller. + mem::forget(frame); + ptr + } + Err(err) => { + let mut buf = String::new(); + + buf += &format!("{}", err.cause()); + + push_backtrace(&mut buf, &err); + + let buf = CString::new(buf.replace('\0', "\\0")).unwrap(); + api.set_filter_error(buf.as_ptr(), frame_ctx); + + ptr::null() + } + } + } + _ => ptr::null(), + }; + + mem::forget(filter); + + rv + }; + + match panic::catch_unwind(closure) { + Ok(frame) => frame, + Err(_) => process::abort(), + } +} + +/// Creates a new instance of the filter. +pub(crate) unsafe extern "system" fn create( + in_: *const ffi::VSMap, + out: *mut ffi::VSMap, + user_data: *mut c_void, + core: *mut ffi::VSCore, + api: *const ffi::VSAPI, +) { + let closure = move || { + API::set(api); + + let args = MapRef::from_ptr(in_); + let mut out = MapRefMut::from_ptr(out); + let core = CoreRef::from_ptr(core); + let data = Box::from_raw(user_data as *mut FilterFunctionData); + + let filter = match data.filter_function.create(API::get_cached(), core, &args) { + Ok(Some(filter)) => Some(Box::new(filter)), + Ok(None) => None, + Err(err) => { + let mut buf = String::new(); + + buf += &format!( + "Error in Filter::create() of {}: {}", + data.name.to_str().unwrap(), + err.cause() + ); + + push_backtrace(&mut buf, &err); + + out.set_error(&buf.replace('\0', "\\0")).unwrap(); + None + } + }; + + if let Some(filter) = filter { + API::get_cached().create_filter( + in_, + out.deref_mut().deref_mut(), + data.name.as_ptr(), + init, + get_frame, + Some(free), + ffi::VSFilterMode::fmParallel, + ffi::VSNodeFlags(0), + Box::into_raw(filter) as *mut _, + core.ptr(), + ); + } + + mem::forget(data); + }; + + if panic::catch_unwind(closure).is_err() { + // The `FilterFunction` might have been left in an inconsistent state, so we have to abort. + process::abort(); + } +} + +/// Registers the plugin. +/// +/// This function is for internal use only. +/// +/// # Safety +/// The caller must ensure the pointers are valid. +#[inline] +pub unsafe fn call_config_func( + config_func: *const c_void, + plugin: *mut c_void, + metadata: Metadata, +) { + let config_func = *(&config_func as *const _ as *const ffi::VSConfigPlugin); + + let identifier_cstring = CString::new(metadata.identifier) + .expect("Couldn't convert the plugin identifier to a CString"); + let namespace_cstring = CString::new(metadata.namespace) + .expect("Couldn't convert the plugin namespace to a CString"); + let name_cstring = + CString::new(metadata.name).expect("Couldn't convert the plugin name to a CString"); + + config_func( + identifier_cstring.as_ptr(), + namespace_cstring.as_ptr(), + name_cstring.as_ptr(), + ffi::VAPOURSYNTH_API_VERSION, + if metadata.read_only { 1 } else { 0 }, + plugin as *mut ffi::VSPlugin, + ); +} + +/// Registers the filter `F`. +/// +/// This function is for internal use only. +/// +/// # Safety +/// The caller must ensure the pointers are valid. +#[inline] +pub unsafe fn call_register_func( + register_func: *const c_void, + plugin: *mut c_void, + filter_function: F, +) { + let register_func = *(®ister_func as *const _ as *const ffi::VSRegisterFunction); + + let name_cstring = CString::new(filter_function.name()) + .expect("Couldn't convert the filter name to a CString"); + let args_cstring = CString::new(filter_function.args()) + .expect("Couldn't convert the filter args to a CString"); + + let data = Box::new(FilterFunctionData { + filter_function, + name: name_cstring, + }); + + register_func( + data.name.as_ptr(), + args_cstring.as_ptr(), + create::, + Box::into_raw(data) as _, + plugin as *mut ffi::VSPlugin, + ); +} + +/// Exports a VapourSynth plugin from this library. +/// +/// This macro should be used only once at the top level of the library. The library should have a +/// `cdylib` crate type. +/// +/// The first parameter is a `Metadata` expression containing your plugin's metadata. +/// +/// Following it is a list of values implementing `FilterFunction`, those are the filter functions +/// the plugin will export. +/// +/// # Example +/// ```ignore +/// export_vapoursynth_plugin! { +/// Metadata { +/// identifier: "com.example.invert", +/// namespace: "invert", +/// name: "Invert Example Plugin", +/// read_only: true, +/// }, +/// [SampleFilterFunction::new(), OtherFunction::new()] +/// } +/// ``` +#[macro_export] +macro_rules! export_vapoursynth_plugin { + ($metadata:expr, [$($filter:expr),*$(,)*]) => ( + use ::std::os::raw::c_void; + + #[allow(non_snake_case)] + #[no_mangle] + pub unsafe extern "system" fn VapourSynthPluginInit( + config_func: *const c_void, + register_func: *const c_void, + plugin: *mut c_void, + ) { + use ::std::{panic, process}; + use $crate::plugins::ffi::{call_config_func, call_register_func}; + + let closure = move || { + call_config_func(config_func, plugin, $metadata); + + $(call_register_func(register_func, plugin, $filter);)* + }; + + if panic::catch_unwind(closure).is_err() { + process::abort(); + } + } + ) +} diff --git a/vapoursynth/src/plugins/frame_context.rs b/vapoursynth/src/plugins/frame_context.rs new file mode 100644 index 0000000..50540d0 --- /dev/null +++ b/vapoursynth/src/plugins/frame_context.rs @@ -0,0 +1,40 @@ +use std::marker::PhantomData; +use std::ptr::NonNull; +use vapoursynth_sys as ffi; + +use api::API; + +/// A frame context used in filters. +#[derive(Debug, Clone, Copy)] +pub struct FrameContext<'a> { + handle: NonNull, + _owner: PhantomData<&'a ()>, +} + +impl<'a> FrameContext<'a> { + /// Wraps `handle` in a `FrameContext`. + /// + /// # Safety + /// The caller must ensure `handle` is valid and API is cached. + #[inline] + pub(crate) unsafe fn from_ptr(handle: *mut ffi::VSFrameContext) -> Self { + Self { + handle: NonNull::new_unchecked(handle), + _owner: PhantomData, + } + } + + /// Returns the underlying pointer. + #[inline] + pub(crate) fn ptr(self) -> *mut ffi::VSFrameContext { + self.handle.as_ptr() + } + + /// Returns the index of the node from which the frame is being requested. + #[inline] + pub fn output_index(self) -> usize { + let index = unsafe { API::get_cached().get_output_index(self.handle.as_ptr()) }; + debug_assert!(index >= 0); + index as _ + } +} diff --git a/vapoursynth/src/plugins/mod.rs b/vapoursynth/src/plugins/mod.rs new file mode 100644 index 0000000..a9bff92 --- /dev/null +++ b/vapoursynth/src/plugins/mod.rs @@ -0,0 +1,464 @@ +//! Things related to making VapourSynth plugins. +use failure::Error; + +use api::API; +use core::CoreRef; +use frame::FrameRef; +use function::Function; +use map::{self, Map, Value, ValueIter}; +use node::Node; +use video_info::VideoInfo; + +mod frame_context; +pub use self::frame_context::FrameContext; + +pub mod ffi; + +/// Plugin metadata. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Metadata { + /// A "reverse" URL, unique among all plugins. + /// + /// For example, `com.example.invert`. + pub identifier: &'static str, + + /// Namespace where the plugin's filters will go, unique among all plugins. + /// + /// Only lowercase letters and the underscore should be used, and it shouldn't be too long. + /// Additionally, words that are special to Python, e.g. `del`, should be avoided. + /// + /// For example, `invert`. + pub namespace: &'static str, + + /// Plugin name in readable form. + /// + /// For example, `Invert Example Plugin`. + pub name: &'static str, + + /// Whether new filters can be registered at runtime. + /// + /// This should generally be set to `false`. It's used for the built-in AviSynth compat plugin. + pub read_only: bool, +} + +/// A filter function interface. +/// +/// See the `make_filter_function!` macro that generates types implementing this automatically. +pub trait FilterFunction: Send + Sync { + /// Returns the name of the function. + /// + /// The characters allowed are letters, numbers, and the underscore. The first character must + /// be a letter. In other words: `^[a-zA-Z][a-zA-Z0-9_]*$`. + /// + /// For example, `Invert`. + fn name(&self) -> &str; + + /// Returns the argument string. + /// + /// Arguments are separated by a semicolon. Each argument is made of several fields separated + /// by a colon. Don’t insert additional whitespace characters, or VapourSynth will die. + /// + /// Fields: + /// - The argument name. The same characters are allowed as for the filter's name. Argument + /// names should be all lowercase and use only letters and the underscore. + /// + /// - The type. One of `int`, `float`, `data`, `clip`, `frame`, `func`. They correspond to the + /// `Map::get_*()` functions (`clip` is `get_node()`). It's possible to declare an array by + /// appending `[]` to the type. + /// + /// - `opt` if the parameter is optional. + /// + /// - `empty` if the array is allowed to be empty. + /// + /// The following example declares the arguments "blah", "moo", and "asdf": + /// `blah:clip;moo:int[]:opt;asdf:float:opt;` + fn args(&self) -> &str; + + /// The callback for this filter function. + /// + /// In most cases this is where you should create a new instance of the filter and return it. + /// However, a filter function like AviSynth compat's `LoadPlugin()` which isn't actually a + /// filter, can return `None`. + /// + /// `args` contains the filter arguments, as specified by the argument string from + /// `FilterFunction::args()`. Their presence and types are validated by VapourSynth so it's + /// safe to `unwrap()`. + /// + /// In this function you should take all input nodes for your filter and store them somewhere + /// so that you can request their frames in `get_frame_initial()`. + // TODO: with generic associated types it'll be possible to make Filter<'core> an associated + // type of this trait and get rid of this Box. + fn create<'core>( + &self, + api: API, + core: CoreRef<'core>, + args: &Map<'core>, + ) -> Result + 'core>>, Error>; +} + +/// A filter interface. +// TODO: perhaps it's possible to figure something out about Send + Sync with specialization? Since +// there are Node flags which say that the filter will be called strictly by one thread, in which +// case Sync shouldn't be required. +pub trait Filter<'core>: Send + Sync { + /// Returns the parameters of this filter's output node. + /// + /// The returned vector should contain one entry for each node output index. + fn video_info(&self, api: API, core: CoreRef<'core>) -> Vec>; + + /// Requests the necessary frames from downstream nodes. + /// + /// This is always the first function to get called for a given frame `n`. + /// + /// In this function you should call `request_frame_filter()` on any input nodes that you need + /// and return `None`. If you do not need any input frames, you should generate the output + /// frame and return it here. + /// + /// Do not call `Node::get_frame()` from within this function. + fn get_frame_initial( + &self, + api: API, + core: CoreRef<'core>, + context: FrameContext, + n: usize, + ) -> Result>, Error>; + + /// Returns the requested frame. + /// + /// This is always the second function to get called for a given frame `n`. If the frame was + /// retrned from `get_frame_initial()`, this function is not called. + /// + /// In this function you should call `get_frame_filter()` on the input nodes to retrieve the + /// frames you requested in `get_frame_initial()`. + /// + /// Do not call `Node::get_frame()` from within this function. + fn get_frame( + &self, + api: API, + core: CoreRef<'core>, + context: FrameContext, + n: usize, + ) -> Result, Error>; +} + +/// An internal trait representing a filter argument type. +pub trait FilterArgument<'map, 'elem: 'map>: Value<'map, 'elem> + private::Sealed { + /// Returns the VapourSynth type name for this argument type. + fn type_name() -> &'static str; +} + +/// An internal trait representing a filter parameter type (argument type + whether it's an array +/// or optional). +pub trait FilterParameter<'map, 'elem: 'map>: private::Sealed { + /// The underlying argument type for this parameter type. + type Argument: FilterArgument<'map, 'elem>; + + /// Returns whether this parameter is an array. + fn is_array() -> bool; + + /// Returns whether this parameter is optional. + fn is_optional() -> bool; + + /// Retrieves this parameter from the given map. + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Self; +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for i64 { + #[inline] + fn type_name() -> &'static str { + "int" + } +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for f64 { + #[inline] + fn type_name() -> &'static str { + "float" + } +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for &'map [u8] { + #[inline] + fn type_name() -> &'static str { + "data" + } +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for Node<'elem> { + #[inline] + fn type_name() -> &'static str { + "clip" + } +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for FrameRef<'elem> { + #[inline] + fn type_name() -> &'static str { + "frame" + } +} + +impl<'map, 'elem: 'map> FilterArgument<'map, 'elem> for Function<'elem> { + #[inline] + fn type_name() -> &'static str { + "func" + } +} + +impl<'map, 'elem: 'map, T> FilterParameter<'map, 'elem> for T +where + T: FilterArgument<'map, 'elem>, +{ + type Argument = Self; + + #[inline] + fn is_array() -> bool { + false + } + + #[inline] + fn is_optional() -> bool { + false + } + + #[inline] + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Self { + Self::get_from_map(map, key).unwrap() + } +} + +impl<'map, 'elem: 'map, T> FilterParameter<'map, 'elem> for Option +where + T: FilterArgument<'map, 'elem>, +{ + type Argument = T; + + #[inline] + fn is_array() -> bool { + false + } + + #[inline] + fn is_optional() -> bool { + true + } + + #[inline] + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Self { + match ::get_from_map(map, key) { + Ok(x) => Some(x), + Err(map::Error::KeyNotFound) => None, + _ => unreachable!(), + } + } +} + +impl<'map, 'elem: 'map, T> FilterParameter<'map, 'elem> for ValueIter<'map, 'elem, T> +where + T: FilterArgument<'map, 'elem>, +{ + type Argument = T; + + #[inline] + fn is_array() -> bool { + true + } + + #[inline] + fn is_optional() -> bool { + false + } + + #[inline] + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Self { + ::get_iter_from_map(map, key).unwrap() + } +} + +impl<'map, 'elem: 'map, T> FilterParameter<'map, 'elem> for Option> +where + T: FilterArgument<'map, 'elem>, +{ + type Argument = T; + + #[inline] + fn is_array() -> bool { + true + } + + #[inline] + fn is_optional() -> bool { + true + } + + #[inline] + fn get_from_map(map: &'map Map<'elem>, key: &str) -> Self { + match ::get_iter_from_map(map, key) { + Ok(x) => Some(x), + Err(map::Error::KeyNotFound) => None, + _ => unreachable!(), + } + } +} + +mod private { + use super::{FilterArgument, FrameRef, Function, Node, ValueIter}; + + pub trait Sealed {} + + impl Sealed for i64 {} + impl Sealed for f64 {} + impl<'map> Sealed for &'map [u8] {} + impl<'elem> Sealed for Node<'elem> {} + impl<'elem> Sealed for FrameRef<'elem> {} + impl<'elem> Sealed for Function<'elem> {} + + impl<'map, 'elem: 'map, T> Sealed for Option where T: FilterArgument<'map, 'elem> {} + + impl<'map, 'elem: 'map, T> Sealed for ValueIter<'map, 'elem, T> where T: FilterArgument<'map, 'elem> {} + + impl<'map, 'elem: 'map, T> Sealed for Option> where + T: FilterArgument<'map, 'elem> + {} +} + +/// Make a filter function easily and avoid boilerplate. +/// +/// This macro accepts the name of the filter function type, the name of the filter and the create +/// function. +/// +/// The macro generates a type implementing `FilterFunction` with the correct `args()` string +/// derived from the function parameters of the specified create function. The generated +/// `FilterFunction::create()` extracts all parameters from the argument map received from +/// VapourSynth and passes them into the specified create function. +/// +/// The create function should look like: +/// +/// ```ignore +/// fn create<'core>( +/// api: API, +/// core: CoreRef<'core>, +/// /* filter arguments */ +/// ) -> Result + 'core>>, Error> { +/// /* ... */ +/// } +/// ``` +/// +/// All VapourSynth-supported types can be used, as well as `Option` for optional parameters and +/// `ValueIter` for array parameters. Array parameters can be empty. +/// +/// Caveat: the macro doesn't currently allow specifying mutable parameters, so to do that they +/// have to be reassigned to a mutable variable in the function body. This is mainly a problem for +/// array parameters. See how the example below handles it. +/// +/// Another caveat: underscore lifetimes are required for receiving `ValueIter`. +/// +/// # Example +/// ```ignore +/// make_filter_function! { +/// MyFilterFunction, "MyFilter" +/// +/// fn create_my_filter<'core>( +/// _api: API, +/// _core: CoreRef<'core>, +/// int_parameter: i64, +/// some_data: &[u8], +/// optional_parameter: Option, +/// array_parameter: ValueIter<'_, 'core, Node<'core>>, +/// optional_array_parameter: Option>>, +/// ) -> Result + 'core>>, Error> { +/// let mut array_parameter = array_parameter; +/// Ok(Some(Box::new(MyFilter::new(/* ... */)))); +/// } +/// } +/// ``` +#[macro_export] +macro_rules! make_filter_function { + ( + $struct_name:ident, $function_name:tt + + $(#[$attr:meta])* + fn $create_fn_name:ident<$lifetime:tt>( + $api_arg_name:ident : $api_arg_type:ty, + $core_arg_name:ident : $core_arg_type:ty, + $($arg_name:ident : $arg_type:ty),* $(,)* + ) -> $return_type:ty { + $($body:tt)* + } + ) => ( + struct $struct_name { + args: String, + } + + impl $struct_name { + fn new<'core>() -> Self { + let mut args = String::new(); + + $( + // Don't use format!() for better constant propagation. + args += stringify!($arg_name); // TODO: allow using a different name. + args += ":"; + args + += <<$arg_type as $crate::plugins::FilterParameter>::Argument>::type_name(); + + if <$arg_type as $crate::plugins::FilterParameter>::is_array() { + args += "[]"; + } + + if <$arg_type as $crate::plugins::FilterParameter>::is_optional() { + args += ":opt"; + } + + // TODO: allow specifying this. + if <$arg_type as $crate::plugins::FilterParameter>::is_array() { + args += ":empty"; + } + + args += ";"; + )* + + Self { args } + } + } + + impl $crate::plugins::FilterFunction for $struct_name { + #[inline] + fn name(&self) -> &str { + $function_name + } + + #[inline] + fn args(&self) -> &str { + &self.args + } + + #[inline] + fn create<'core>( + &self, + api: API, + core: CoreRef<'core>, + args: &Map<'core>, + ) -> Result + 'core>>, Error> { + $create_fn_name( + api, + core, + $( + <$arg_type as $crate::plugins::FilterParameter>::get_from_map( + args, + stringify!($arg_name), + ) + ),* + ) + } + } + + $(#[$attr])* + fn $create_fn_name<$lifetime>( + $api_arg_name : $api_arg_type, + $core_arg_name : $core_arg_type, + $($arg_name : $arg_type),* + ) -> $return_type { + $($body)* + } + ) +} diff --git a/vapoursynth/src/tests.rs b/vapoursynth/src/tests.rs new file mode 100644 index 0000000..2fbcfd1 --- /dev/null +++ b/vapoursynth/src/tests.rs @@ -0,0 +1,905 @@ +#![cfg(test)] +use super::*; + +// We need the VSScript functions, and either VSScript API 3.2 or the VapourSynth functions. +#[cfg(all( + feature = "vsscript-functions", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) +))] +mod need_api_and_vsscript { + use std::ffi::CStr; + use std::fmt::Debug; + use std::mem; + use std::slice; + use std::sync::mpsc::channel; + + use super::*; + use function::Function; + use prelude::*; + use video_info::{Framerate, Resolution}; + + fn props_test(frame: &Frame, fps_num: i64) { + let props = frame.props(); + assert_eq!(props.key_count(), 2); + assert_eq!(props.key(0), "_DurationDen"); + assert_eq!(props.key(1), "_DurationNum"); + + assert_eq!(props.value_count(props.key(0)), Ok(1)); + assert_eq!(props.get_int(props.key(0)), Ok(fps_num)); + assert_eq!(props.value_count(props.key(1)), Ok(1)); + assert_eq!(props.get_int(props.key(1)), Ok(1)); + } + + fn env_video_var_test(env: &vsscript::Environment) { + let mut map = OwnedMap::new(API::get().unwrap()); + assert!(env.get_variable("video", &mut map).is_ok()); + assert_eq!(map.key_count(), 1); + assert_eq!(map.key(0), "video"); + let node = map.get_node("video"); + assert!(node.is_ok()); + } + + fn green_frame_test(frame: &Frame) { + let format = frame.format(); + assert_eq!(format.name(), "RGB24"); + assert_eq!(format.plane_count(), 3); + + for plane in 0..format.plane_count() { + let resolution = frame.resolution(plane); + assert_eq!( + resolution, + Resolution { + width: 1920, + height: 1080, + } + ); + + let color = if plane == 1 { [255; 1920] } else { [0; 1920] }; + + for row in 0..resolution.height { + let data_row = frame.data_row(plane, row); + assert_eq!(&data_row[..], &color[..]); + let data_row = frame.plane_row::(plane, row); + assert_eq!(&data_row[..], &color[..]); + } + } + } + + fn green_test(env: &vsscript::Environment) { + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = { + let output = env.get_output(0); + assert!(output.is_ok()); + output.unwrap() + }; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = (env.get_output(0).unwrap(), None::); + + assert!(alpha_node.is_none()); + + let info = node.info(); + + if let Property::Constant(format) = info.format { + assert_eq!(format.name(), "RGB24"); + } else { + assert!(false); + } + + assert_eq!( + info.framerate, + Property::Constant(Framerate { + numerator: 60, + denominator: 1, + }) + ); + assert_eq!( + info.resolution, + Property::Constant(Resolution { + width: 1920, + height: 1080, + }) + ); + + #[cfg(feature = "gte-vapoursynth-api-32")] + assert_eq!(info.num_frames, 100); + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + assert_eq!(info.num_frames, Property::Constant(100)); + + let frame = node.get_frame(0).unwrap(); + green_frame_test(&frame); + props_test(&frame, 60); + env_video_var_test(&env); + } + + #[test] + fn green() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + green_test(&env); + } + + #[test] + fn green_from_string() { + let env = + vsscript::Environment::from_script(include_str!("../test-vpy/green.vpy")).unwrap(); + green_test(&env); + } + + #[test] + fn variable() { + let env = + vsscript::Environment::from_file("test-vpy/variable.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = { + let output = env.get_output(0); + assert!(output.is_ok()); + output.unwrap() + }; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = (env.get_output(0).unwrap(), None::); + + assert!(alpha_node.is_none()); + + let info = node.info(); + + assert_eq!(info.format, Property::Variable); + assert_eq!(info.framerate, Property::Variable); + assert_eq!(info.resolution, Property::Variable); + + #[cfg(feature = "gte-vapoursynth-api-32")] + assert_eq!(info.num_frames, 200); + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + assert_eq!(info.num_frames, Property::Constant(200)); + + // Test the first frame. + let frame = node.get_frame(0).unwrap(); + let format = frame.format(); + assert_eq!(format.name(), "RGB24"); + assert_eq!(format.plane_count(), 3); + + for plane in 0..format.plane_count() { + let resolution = frame.resolution(plane); + assert_eq!( + resolution, + Resolution { + width: 1920, + height: 1080, + } + ); + + let color = if plane == 1 { [255; 1920] } else { [0; 1920] }; + + for row in 0..resolution.height { + let data_row = frame.data_row(plane, row); + assert_eq!(&data_row[..], &color[..]); + let data_row = frame.plane_row::(plane, row); + assert_eq!(&data_row[..], &color[..]); + } + } + + props_test(&frame, 60); + + // Test the first frame of the next format. + let frame = node.get_frame(100).unwrap(); + let format = frame.format(); + assert_eq!(format.name(), "Gray8"); + assert_eq!(format.plane_count(), 1); + + let plane = 0; + let resolution = frame.resolution(plane); + assert_eq!( + resolution, + Resolution { + width: 1280, + height: 720, + } + ); + + let color = [127; 1280]; + + for row in 0..resolution.height { + let data_row = frame.data_row(plane, row); + assert_eq!(&data_row[..], &color[..]); + let data_row = frame.plane_row::(plane, row); + assert_eq!(&data_row[..], &color[..]); + } + + props_test(&frame, 30); + env_video_var_test(&env); + } + + #[test] + #[cfg(feature = "gte-vsscript-api-31")] + fn alpha() { + let env = + vsscript::Environment::from_file("test-vpy/alpha.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + let (_, alpha_node) = { + let output = env.get_output(0); + assert!(output.is_ok()); + output.unwrap() + }; + + assert!(alpha_node.is_some()); + let alpha_node = alpha_node.unwrap(); + + let info = alpha_node.info(); + + if let Property::Constant(format) = info.format { + assert_eq!(format.name(), "Gray8"); + } else { + assert!(false); + } + + assert_eq!( + info.framerate, + Property::Constant(Framerate { + numerator: 60, + denominator: 1, + }) + ); + assert_eq!( + info.resolution, + Property::Constant(Resolution { + width: 1920, + height: 1080, + }) + ); + + #[cfg(feature = "gte-vapoursynth-api-32")] + assert_eq!(info.num_frames, 100); + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + assert_eq!(info.num_frames, Property::Constant(100)); + + let frame = alpha_node.get_frame(0).unwrap(); + let format = frame.format(); + assert_eq!(format.name(), "Gray8"); + assert_eq!(format.plane_count(), 1); + + let resolution = frame.resolution(0); + assert_eq!( + resolution, + Resolution { + width: 1920, + height: 1080, + } + ); + + for row in 0..resolution.height { + let data_row = frame.data_row(0, row); + assert_eq!(&data_row[..], &[128; 1920][..]); + let data_row = frame.plane_row::(0, row); + assert_eq!(&data_row[..], &[128; 1920][..]); + } + } + + fn verify_pixel_format( + env: &Environment, + index: i32, + bits_per_sample: u8, + color: [T; 3], + ) { + #[cfg(feature = "gte-vsscript-api-31")] + let node = env.get_output(index).unwrap().0; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let node = env.get_output(index).unwrap(); + + let frame = node.get_frame(0).unwrap(); + let format = frame.format(); + + assert_eq!(format.bits_per_sample(), bits_per_sample); + let bytes_per_sample = ((bits_per_sample + 7) / 8).next_power_of_two(); + assert_eq!(format.bytes_per_sample(), bytes_per_sample); + + for plane_num in 0..3 { + // Compare the entire row at once for speed. + let row_gt = vec![color[plane_num]; frame.width(plane_num)]; + + for y in 0..frame.height(plane_num) { + let row = frame.plane_row(plane_num, y); + assert_eq!(row.len(), frame.width(plane_num)); + assert_eq!(&row_gt[..], row); + } + + if let Ok(data) = frame.plane(plane_num) { + assert_eq!(data.len(), frame.height(plane_num) * frame.width(plane_num)); + + for y in 0..frame.height(plane_num) { + assert_eq!( + &row_gt[..], + &data[y * frame.width(plane_num)..(y + 1) * frame.width(plane_num)] + ); + } + } + } + } + + #[test] + fn pixel_formats() { + let env = vsscript::Environment::from_file( + "test-vpy/pixel-formats.vpy", + vsscript::EvalFlags::Nothing, + ).unwrap(); + + verify_pixel_format(&env, 0, 10, [789u16, 123u16, 456u16]); + verify_pixel_format(&env, 1, 32, [5f32, 42f32, 0.25f32]); + verify_pixel_format(&env, 2, 32, [0.125f32, 10f32, 0.5f32]); + verify_pixel_format(&env, 3, 17, [77777u32, 88888u32, 99999u32]); + verify_pixel_format(&env, 4, 32, [u32::max_value(), 12345u32, 65432u32]); + + #[cfg(feature = "f16-pixel-type")] + verify_pixel_format( + &env, + 5, + 16, + [ + half::f16::from_f32(0.0625f32), + half::f16::from_f32(5f32), + half::f16::from_f32(0.25f32), + ], + ); + } + + #[test] + #[should_panic] + fn invalid_component_type() { + let env = vsscript::Environment::from_file( + "test-vpy/pixel-formats.vpy", + vsscript::EvalFlags::Nothing, + ).unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let node = env.get_output(0).unwrap().0; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let node = env.get_output(0).unwrap(); + + let frame = node.get_frame(0).unwrap(); + let _ = frame.plane_row::(0, 0); // Should be u16. + } + + #[test] + fn gradient() { + let env = + vsscript::Environment::from_file("test-vpy/gradient.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let node = env.get_output(0).unwrap().0; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let node = env.get_output(0).unwrap(); + + let frame = node.get_frame(0).unwrap(); + for plane in 0..3 { + for row in 0..16 { + let mut gt = [0u8; 16]; + for col in 0..16 { + gt[col] = match plane { + 0 => row as u8 * 16, + 1 => col as u8 * 16, + 2 => 0, + _ => unreachable!(), + }; + } + + let data = frame.data_row(plane, row); + assert_eq!(data, >[..]); + let data = frame.plane_row::(plane, row); + assert_eq!(data, >[..]); + } + } + } + + #[test] + fn clear_output() { + let env = + vsscript::Environment::from_script(include_str!("../test-vpy/green.vpy")).unwrap(); + assert!( + env.clear_output(1) + .err() + .map(|e| if let vsscript::Error::NoOutput = e { + true + } else { + false + }).unwrap_or(false) + ); + assert!(env.clear_output(0).is_ok()); + assert!( + env.clear_output(0) + .err() + .map(|e| if let vsscript::Error::NoOutput = e { + true + } else { + false + }).unwrap_or(false) + ); + } + + #[test] + fn iterators() { + let env = + vsscript::Environment::from_script(include_str!("../test-vpy/green.vpy")).unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = env.get_output(0).unwrap(); + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = (env.get_output(0).unwrap(), None::); + + assert!(alpha_node.is_none()); + + let frame = node.get_frame(0).unwrap(); + let props = frame.props(); + + assert_eq!(props.keys().size_hint(), (2, Some(2))); + // assert_eq!(props.iter().size_hint(), (2, Some(2))); + } + + #[test] + fn vsscript_variables() { + let env = + vsscript::Environment::from_script(include_str!("../test-vpy/green.vpy")).unwrap(); + + let mut map = OwnedMap::new(API::get().unwrap()); + assert!(env.get_variable("video", &mut map).is_ok()); + assert!(env.clear_variable("video").is_ok()); + assert!(env.clear_variable("video").is_err()); + assert!(env.get_variable("video", &mut map).is_err()); + + assert!(env.set_variables(&map).is_ok()); + assert!(env.get_variable("video", &mut map).is_ok()); + } + + #[test] + fn get_frame_async() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = { + let output = env.get_output(0); + assert!(output.is_ok()); + output.unwrap() + }; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = (env.get_output(0).unwrap(), None::); + + assert!(alpha_node.is_none()); + + let mut rxs = Vec::new(); + + for i in 0..10 { + let (tx, rx) = channel(); + rxs.push(rx); + + node.get_frame_async(i, move |frame, n, node| { + assert!(frame.is_ok()); + let frame = frame.unwrap(); + + assert_eq!(n, i); + assert_eq!( + node.info().framerate, + Property::Constant(Framerate { + numerator: 60, + denominator: 1, + }) + ); + + green_frame_test(&frame); + props_test(&frame, 60); + + assert_eq!(tx.send(()), Ok(())); + }); + } + + drop(node); // Test dropping prematurely. + + for rx in rxs { + assert_eq!(rx.recv(), Ok(())); + } + } + + #[test] + fn get_frame_async_error() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, alpha_node) = { + let output = env.get_output(0); + assert!(output.is_ok()); + output.unwrap() + }; + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, alpha_node) = (env.get_output(0).unwrap(), None::); + + assert!(alpha_node.is_none()); + + let (tx, rx) = channel(); + + // The clip only has 100 frames, so requesting the 101th one produces an error. + node.get_frame_async(100, move |frame, n, node| { + assert!(frame.is_err()); + assert_eq!(n, 100); + assert_eq!( + node.info().framerate, + Property::Constant(Framerate { + numerator: 60, + denominator: 1, + }) + ); + + assert_eq!(tx.send(()), Ok(())); + }); + + assert_eq!(rx.recv(), Ok(())); + } + + #[test] + fn core() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + let core = env.get_core(); + assert!(core.is_ok()); + let core = core.unwrap(); + + let yuv420p8 = core.get_format(PresetFormat::YUV420P8.into()); + assert!(yuv420p8.is_some()); + let yuv420p8 = yuv420p8.unwrap(); + + assert_eq!(yuv420p8.id(), PresetFormat::YUV420P8.into()); + assert_eq!(yuv420p8.name(), "YUV420P8"); + assert_eq!(yuv420p8.plane_count(), 3); + assert_eq!(yuv420p8.color_family(), ColorFamily::YUV); + assert_eq!(yuv420p8.sample_type(), SampleType::Integer); + assert_eq!(yuv420p8.bits_per_sample(), 8); + assert_eq!(yuv420p8.bytes_per_sample(), 1); + assert_eq!(yuv420p8.sub_sampling_w(), 1); + assert_eq!(yuv420p8.sub_sampling_h(), 1); + + let yuv422p8 = core.get_format(PresetFormat::YUV422P8.into()).unwrap(); + assert_eq!(yuv422p8.sub_sampling_w(), 1); + assert_eq!(yuv422p8.sub_sampling_h(), 0); + } + + #[test] + fn plugins() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + fn bind<'a, T: ?Sized, U: ?Sized>(_: &'a T, x: &'a U) -> &'a U { + x + } + + let core = env.get_core().unwrap(); + let plugins = core.plugins(); + let ids: Vec<_> = plugins + .keys() + .filter_map(|key| unsafe { + bind( + key, + CStr::from_ptr(plugins.get_data(key).unwrap().as_ptr() as _), + ).to_str() + .ok() + }).filter_map(|id| id.split(';').nth(1)) + .collect(); + assert!(ids.contains(&"com.vapoursynth.std")); + assert!(ids.contains(&"com.vapoursynth.resize")); + + let std = core.get_plugin_by_id("com.vapoursynth.std"); + assert!(std.is_ok()); + let std = std.unwrap(); + assert!(std.is_some()); + let std = std.unwrap(); + + let functions = std.functions(); + let names: Vec<_> = functions + .keys() + .filter_map(|key| unsafe { + bind( + key, + CStr::from_ptr(functions.get_data(key).unwrap().as_ptr() as _), + ).to_str() + .ok() + .map(|value| (key, value)) + }).filter_map(|(key, value)| value.split(';').nth(0).map(|name| (key, name))) + .collect(); + assert!(names.contains(&("CropRel", "CropRel"))); + + #[cfg(feature = "gte-vsscript-api-31")] + let (node, _) = env.get_output(0).unwrap(); + #[cfg(not(feature = "gte-vsscript-api-31"))] + let (node, _) = (env.get_output(0).unwrap(), None::); + + let mut args = OwnedMap::new(API::get().unwrap()); + args.set_node("clip", &node); + args.set_int("left", 100); + + let rv = std.invoke("CropRel", &args).unwrap(); + assert!(rv.error().is_none()); + + let node = rv.get_node("clip"); + assert!(node.is_ok()); + let node = node.unwrap(); + + let frame = node.get_frame(0).unwrap(); + assert_eq!( + frame.resolution(0), + Resolution { + width: 1820, + height: 1080, + } + ); + } + + #[test] + fn functions() { + let env = + vsscript::Environment::from_file("test-vpy/green.vpy", vsscript::EvalFlags::Nothing) + .unwrap(); + + let core = env.get_core().unwrap(); + let api = API::get().unwrap(); + + let function = Function::new(api, core, |_api, _core, in_, out| { + assert_eq!(in_.get_int("hello").unwrap(), 1337); + out.set_int("there", 42).unwrap(); + }); + + let mut in_ = OwnedMap::new(api); + let mut out = OwnedMap::new(api); + in_.set_int("hello", 1337).unwrap(); + + function.call(&in_, &mut out); + + assert!(out.error().is_none()); + assert_eq!(out.get_int("there").unwrap(), 42); + } +} + +// We need either VSScript API 3.2 or the VapourSynth functions. +#[cfg(any( + feature = "vapoursynth-functions", + all( + feature = "vsscript-functions", + feature = "gte-vsscript-api-32" + ) +))] +mod need_api { + use std::ffi::CString; + use std::sync::mpsc::{channel, Sender}; + use std::sync::Mutex; + + use super::*; + use prelude::*; + + #[test] + fn maps() { + let mut map = OwnedMap::new(API::get().unwrap()); + + assert_eq!(map.key_count(), 0); + + assert_eq!(map.touch("test_frame", ValueType::Frame), Ok(())); + assert_eq!(map.value_type("test_frame"), Ok(ValueType::Frame)); + assert_eq!(map.value_count("test_frame"), Ok(0)); + assert_eq!( + map.append_int("test_frame", 42), + Err(map::Error::WrongValueType) + ); + assert_eq!( + map.append("test_frame", &42), + Err(map::Error::WrongValueType) + ); + + assert_eq!(map.set_int("i", 42), Ok(())); + assert_eq!(map.get_int("i"), Ok(42)); + assert_eq!(map.append_int("i", 43), Ok(())); + assert_eq!(map.get_int("i"), Ok(42)); + { + let iter = map.get_int_iter("i"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(42)); + assert_eq!(iter.next(), Some(43)); + assert_eq!(iter.next(), None); + } + + assert_eq!(map.set("i", &42), Ok(())); + assert_eq!(map.get("i"), Ok(42)); + assert_eq!(map.append("i", &43), Ok(())); + assert_eq!(map.get("i"), Ok(42)); + { + let iter = map.get_iter::("i"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(42)); + assert_eq!(iter.next(), Some(43)); + assert_eq!(iter.next(), None); + } + + #[cfg(feature = "gte-vapoursynth-api-31")] + { + assert_eq!(map.get_int_array("i"), Ok(&[42, 43][..])); + + assert_eq!(map.set_int_array("ia", &[10, 20, 30]), Ok(())); + assert_eq!(map.get_int_array("ia"), Ok(&[10, 20, 30][..])); + } + + assert_eq!(map.set_float("f", 42f64), Ok(())); + assert_eq!(map.get_float("f"), Ok(42f64)); + assert_eq!(map.append_float("f", 43f64), Ok(())); + assert_eq!(map.get_float("f"), Ok(42f64)); + { + let iter = map.get_float_iter("f"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(42f64)); + assert_eq!(iter.next(), Some(43f64)); + assert_eq!(iter.next(), None); + } + + assert_eq!(map.set("f", &42f64), Ok(())); + assert_eq!(map.get("f"), Ok(42f64)); + assert_eq!(map.append("f", &43f64), Ok(())); + assert_eq!(map.get("f"), Ok(42f64)); + { + let iter = map.get_iter::("f"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(42f64)); + assert_eq!(iter.next(), Some(43f64)); + assert_eq!(iter.next(), None); + } + + #[cfg(feature = "gte-vapoursynth-api-31")] + { + assert_eq!(map.get_float_array("f"), Ok(&[42f64, 43f64][..])); + + assert_eq!(map.set_float_array("fa", &[10f64, 20f64, 30f64]), Ok(())); + assert_eq!(map.get_float_array("fa"), Ok(&[10f64, 20f64, 30f64][..])); + } + + assert_eq!(map.set_data("d", &[1, 2, 3]), Ok(())); + assert_eq!(map.get_data("d"), Ok(&[1, 2, 3][..])); + assert_eq!(map.append_data("d", &[4, 5, 6]), Ok(())); + assert_eq!(map.get_data("d"), Ok(&[1, 2, 3][..])); + { + let iter = map.get_data_iter("d"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(&[1, 2, 3][..])); + assert_eq!(iter.next(), Some(&[4, 5, 6][..])); + assert_eq!(iter.next(), None); + } + + assert_eq!(map.set("d", &&[1, 2, 3][..]), Ok(())); + assert_eq!(map.get("d"), Ok(&[1, 2, 3][..])); + assert_eq!(map.append("d", &&[4, 5, 6][..]), Ok(())); + assert_eq!(map.get("d"), Ok(&[1, 2, 3][..])); + { + let iter = map.get_iter::<&[u8]>("d"); + assert!(iter.is_ok()); + let mut iter = iter.unwrap(); + assert_eq!(iter.next(), Some(&[1, 2, 3][..])); + assert_eq!(iter.next(), Some(&[4, 5, 6][..])); + assert_eq!(iter.next(), None); + } + + // TODO: node, frame and function method tests when we can make them. + + assert_eq!(map.delete_key("test_frame"), Ok(())); + assert_eq!(map.delete_key("test_frame"), Err(map::Error::KeyNotFound)); + + assert_eq!(map.error(), None); + assert_eq!(map.set_error("hello there"), Ok(())); + assert_eq!( + map.error().as_ref().map(|x| x.as_ref()), + Some("hello there") + ); + } + + #[cfg(feature = "gte-vapoursynth-api-34")] + #[test] + fn message_handler() { + let api = API::get().unwrap(); + let (tx, rx) = channel(); + + // Hopefully no one logs anything here and breaks the test. + api.set_message_handler(move |message_type, message| { + assert_eq!(tx.send((message_type, message.to_owned())), Ok(())); + }); + + assert_eq!( + api.log(MessageType::Warning, "test warning message"), + Ok(()) + ); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Warning, + CString::new("test warning message").unwrap() + )) + ); + + assert_eq!(api.log(MessageType::Debug, "test debug message"), Ok(())); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Debug, + CString::new("test debug message").unwrap() + )) + ); + + assert_eq!( + api.log(MessageType::Critical, "test critical message"), + Ok(()) + ); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Critical, + CString::new("test critical message").unwrap() + )) + ); + + { + lazy_static! { + static ref SENDER: Mutex>> = Mutex::new(None); + } + + let (tx, rx) = channel(); + *SENDER.lock().unwrap() = Some(tx); + + api.set_message_handler_trivial(|message_type, message| { + let guard = SENDER.lock().unwrap(); + let tx = guard.as_ref().unwrap(); + assert_eq!(tx.send((message_type, message.to_owned())), Ok(())); + }); + + assert_eq!( + api.log(MessageType::Warning, "test warning message"), + Ok(()) + ); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Warning, + CString::new("test warning message").unwrap() + )) + ); + + assert_eq!(api.log(MessageType::Debug, "test debug message"), Ok(())); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Debug, + CString::new("test debug message").unwrap() + )) + ); + + assert_eq!( + api.log(MessageType::Critical, "test critical message"), + Ok(()) + ); + assert_eq!( + rx.recv(), + Ok(( + MessageType::Critical, + CString::new("test critical message").unwrap() + )) + ); + } + + api.clear_message_handler(); + } +} diff --git a/vapoursynth/src/video_info.rs b/vapoursynth/src/video_info.rs new file mode 100644 index 0000000..db56265 --- /dev/null +++ b/vapoursynth/src/video_info.rs @@ -0,0 +1,183 @@ +//! Video clip formats. + +use std::fmt::Debug; +use std::ops::Deref; +use std::ptr; +use vapoursynth_sys as ffi; + +use format::Format; +use node; + +/// Represents video resolution. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Resolution { + /// Width of the clip, greater than 0. + pub width: usize, + + /// Height of the clip, greater than 0. + pub height: usize, +} + +/// Represents video framerate. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub struct Framerate { + /// FPS numerator, greater than 0. + pub numerator: u64, + + /// FPS denominator, greater than 0. + pub denominator: u64, +} + +/// Represents a property that can be either constant or variable, like the resolution or the +/// framerate. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Property { + /// This property is variable. + Variable, + + /// This property is constant. + Constant(T), +} + +/// Contains information about a video clip. +#[derive(Debug, Copy, Clone)] +pub struct VideoInfo<'core> { + /// Format of the clip. + pub format: Property>, + + /// Framerate of the clip. + pub framerate: Property, + + /// Resolution of the clip. + pub resolution: Property, + + /// Length of the clip, greater than 0. + #[cfg(feature = "gte-vapoursynth-api-32")] + pub num_frames: usize, + + /// Length of the clip. + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + pub num_frames: Property, + + /// The flags of this clip. + pub flags: node::Flags, +} + +impl<'core> VideoInfo<'core> { + /// Creates a `VideoInfo` from a raw pointer. + /// + /// # Safety + /// The caller must ensure `ptr` and the lifetime is valid. + pub(crate) unsafe fn from_ptr(ptr: *const ffi::VSVideoInfo) -> Self { + let info = &*ptr; + + debug_assert!(info.fpsNum >= 0); + debug_assert!(info.fpsDen >= 0); + debug_assert!(info.width >= 0); + debug_assert!(info.height >= 0); + debug_assert!(info.numFrames >= 0); + + let format = if info.format.is_null() { + Property::Variable + } else { + Property::Constant(Format::from_ptr(info.format)) + }; + + let framerate = if info.fpsNum == 0 { + debug_assert!(info.fpsDen == 0); + Property::Variable + } else { + debug_assert!(info.fpsDen != 0); + Property::Constant(Framerate { + numerator: info.fpsNum as _, + denominator: info.fpsDen as _, + }) + }; + + let resolution = if info.width == 0 { + debug_assert!(info.height == 0); + Property::Variable + } else { + debug_assert!(info.height != 0); + Property::Constant(Resolution { + width: info.width as _, + height: info.height as _, + }) + }; + + #[cfg(feature = "gte-vapoursynth-api-32")] + let num_frames = { + debug_assert!(info.numFrames != 0); + info.numFrames as _ + }; + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + let num_frames = { + if info.numFrames == 0 { + Property::Variable + } else { + Property::Constant(info.numFrames as _) + } + }; + + Self { + format, + framerate, + resolution, + num_frames, + flags: ffi::VSNodeFlags(info.flags).into(), + } + } + + /// Converts the Rust struct into a C struct. + pub(crate) fn ffi_type(self) -> ffi::VSVideoInfo { + let format = match self.format { + Property::Variable => ptr::null(), + Property::Constant(x) => x.deref(), + }; + + let (fps_num, fps_den) = match self.framerate { + Property::Variable => (0, 0), + Property::Constant(Framerate { + numerator, + denominator, + }) => (numerator as i64, denominator as i64), + }; + + let (width, height) = match self.resolution { + Property::Variable => (0, 0), + Property::Constant(Resolution { width, height }) => (width as i32, height as i32), + }; + + #[cfg(feature = "gte-vapoursynth-api-32")] + let num_frames = self.num_frames as i32; + + #[cfg(not(feature = "gte-vapoursynth-api-32"))] + let num_frames = match self.num_frames { + Property::Variable => 0, + Property::Constant(x) => x as i32, + }; + + let flags = self.flags.bits(); + + ffi::VSVideoInfo { + format, + fpsNum: fps_num, + fpsDen: fps_den, + width, + height, + numFrames: num_frames, + flags, + } + } +} + +impl From for Property +where + T: Debug + Clone + Copy + Eq + PartialEq, +{ + #[inline] + fn from(x: T) -> Self { + Property::Constant(x) + } +} diff --git a/vapoursynth/src/vsscript/environment.rs b/vapoursynth/src/vsscript/environment.rs new file mode 100644 index 0000000..83bdd5d --- /dev/null +++ b/vapoursynth/src/vsscript/environment.rs @@ -0,0 +1,278 @@ +use std::ffi::{CStr, CString}; +use std::fs::File; +use std::io::Read; +use std::ops::{Deref, DerefMut}; +use std::path::Path; +use std::ptr; +use std::ptr::NonNull; +use vapoursynth_sys as ffi; + +use api::API; +use core::CoreRef; +use map::Map; +use node::Node; +use vsscript::errors::Result; +use vsscript::*; + +/// VSScript file evaluation flags. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum EvalFlags { + Nothing, + /// The working directory will be changed to the script's directory for the evaluation. + SetWorkingDir, +} + +impl EvalFlags { + #[inline] + fn ffi_type(self) -> ::std::os::raw::c_int { + match self { + EvalFlags::Nothing => 0, + EvalFlags::SetWorkingDir => ffi::VSEvalFlags::efSetWorkingDir as _, + } + } +} + +/// Contains two possible variants of arguments to `Environment::evaluate_script()`. +#[derive(Clone, Copy)] +enum EvaluateScriptArgs<'a> { + /// Evaluate a script contained in the string. + Script(&'a str), + /// Evaluate a script contained in the file. + File(&'a Path, EvalFlags), +} + +/// A wrapper for the VSScript environment. +#[derive(Debug)] +pub struct Environment { + handle: NonNull, +} + +unsafe impl Send for Environment {} +unsafe impl Sync for Environment {} + +impl Drop for Environment { + #[inline] + fn drop(&mut self) { + unsafe { + ffi::vsscript_freeScript(self.handle.as_ptr()); + } + } +} + +impl Environment { + /// Retrieves the VSScript error message. + /// + /// # Safety + /// This function must only be called if an error is present. + #[inline] + unsafe fn error(&self) -> CString { + let message = ffi::vsscript_getError(self.handle.as_ptr()); + CStr::from_ptr(message).to_owned() + } + + /// Creates an empty script environment. + /// + /// Useful if it is necessary to set some variable in the script environment before evaluating + /// any scripts. + pub fn new() -> Result { + maybe_initialize(); + + let mut handle = ptr::null_mut(); + let rv = unsafe { call_vsscript!(ffi::vsscript_createScript(&mut handle)) }; + let environment = Self { + handle: unsafe { NonNull::new_unchecked(handle) }, + }; + + if rv != 0 { + Err(VSScriptError::new(unsafe { environment.error() }).into()) + } else { + Ok(environment) + } + } + + /// Calls `vsscript_evaluateScript()`. + /// + /// `self` is taken by a mutable reference mainly to ensure the atomicity of a call to + /// `vsscript_evaluateScript()` (a function that could produce an error) and the following call + /// to `vsscript_getError()`. If atomicity is not enforced, another thread could perform some + /// operation between these two and clear or change the error message. + fn evaluate_script(&mut self, args: EvaluateScriptArgs) -> Result<()> { + let (script, path, flags) = match args { + EvaluateScriptArgs::Script(script) => (script.to_owned(), None, EvalFlags::Nothing), + EvaluateScriptArgs::File(path, flags) => { + let mut file = File::open(path).map_err(Error::FileOpen)?; + let mut script = String::new(); + file.read_to_string(&mut script).map_err(Error::FileRead)?; + + // vsscript throws an error if it's not valid UTF-8 anyway. + let path = path.to_str().ok_or(Error::PathInvalidUnicode)?; + let path = CString::new(path)?; + + (script, Some(path), flags) + } + }; + + let script = CString::new(script)?; + + let rv = unsafe { + call_vsscript!(ffi::vsscript_evaluateScript( + &mut self.handle.as_ptr(), + script.as_ptr(), + path.as_ref().map(|p| p.as_ptr()).unwrap_or(ptr::null()), + flags.ffi_type(), + )) + }; + + if rv != 0 { + Err(VSScriptError::new(unsafe { self.error() }).into()) + } else { + Ok(()) + } + } + + /// Creates a script environment and evaluates a script contained in a string. + #[inline] + pub fn from_script(script: &str) -> Result { + let mut environment = Self::new()?; + environment.evaluate_script(EvaluateScriptArgs::Script(script))?; + Ok(environment) + } + + /// Creates a script environment and evaluates a script contained in a file. + #[inline] + pub fn from_file>(path: P, flags: EvalFlags) -> Result { + let mut environment = Self::new()?; + environment.evaluate_script(EvaluateScriptArgs::File(path.as_ref(), flags))?; + Ok(environment) + } + + /// Evaluates a script contained in a string. + #[inline] + pub fn eval_script(&mut self, script: &str) -> Result<()> { + self.evaluate_script(EvaluateScriptArgs::Script(script)) + } + + /// Evaluates a script contained in a file. + #[inline] + pub fn eval_file>(&mut self, path: P, flags: EvalFlags) -> Result<()> { + self.evaluate_script(EvaluateScriptArgs::File(path.as_ref(), flags)) + } + + /// Clears the script environment. + #[inline] + pub fn clear(&self) { + unsafe { + ffi::vsscript_clearEnvironment(self.handle.as_ptr()); + } + } + + /// Retrieves a node from the script environment. A node in the script must have been marked + /// for output with the requested index. + #[cfg(all( + not(feature = "gte-vsscript-api-31"), + feature = "vapoursynth-functions" + ))] + #[inline] + pub fn get_output(&self, index: i32) -> Result { + // Node needs the API. + API::get().ok_or(Error::NoAPI)?; + + let node_handle = unsafe { ffi::vsscript_getOutput(self.handle.as_ptr(), index) }; + if node_handle.is_null() { + Err(Error::NoOutput) + } else { + Ok(unsafe { Node::from_ptr(node_handle) }) + } + } + + /// Retrieves a node from the script environment. A node in the script must have been marked + /// for output with the requested index. The second node, if any, contains the alpha clip. + #[cfg(all( + feature = "gte-vsscript-api-31", + any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ) + ))] + #[inline] + pub fn get_output(&self, index: i32) -> Result<(Node, Option)> { + // Node needs the API. + API::get().ok_or(Error::NoAPI)?; + + let mut alpha_handle = ptr::null_mut(); + let node_handle = + unsafe { ffi::vsscript_getOutput2(self.handle.as_ptr(), index, &mut alpha_handle) }; + + if node_handle.is_null() { + return Err(Error::NoOutput); + } + + let node = unsafe { Node::from_ptr(node_handle) }; + let alpha_node = unsafe { alpha_handle.as_mut().map(|p| Node::from_ptr(p)) }; + + Ok((node, alpha_node)) + } + + /// Cancels a node set for output. The node will no longer be available to `get_output()`. + #[inline] + pub fn clear_output(&self, index: i32) -> Result<()> { + let rv = unsafe { ffi::vsscript_clearOutput(self.handle.as_ptr(), index) }; + if rv != 0 { + Err(Error::NoOutput) + } else { + Ok(()) + } + } + + /// Retrieves the VapourSynth core that was created in the script environment. If a VapourSynth + /// core has not been created yet, it will be created now, with the default options. + #[cfg(any( + feature = "vapoursynth-functions", + feature = "gte-vsscript-api-32" + ))] + pub fn get_core(&self) -> Result { + // CoreRef needs the API. + API::get().ok_or(Error::NoAPI)?; + + let ptr = unsafe { ffi::vsscript_getCore(self.handle.as_ptr()) }; + if ptr.is_null() { + Err(Error::NoCore) + } else { + Ok(unsafe { CoreRef::from_ptr(ptr) }) + } + } + + /// Retrieves a variable from the script environment. + pub fn get_variable(&self, name: &str, map: &mut Map) -> Result<()> { + let name = CString::new(name)?; + let rv = unsafe { + ffi::vsscript_getVariable(self.handle.as_ptr(), name.as_ptr(), map.deref_mut()) + }; + if rv != 0 { + Err(Error::NoSuchVariable) + } else { + Ok(()) + } + } + + /// Sets variables in the script environment. + pub fn set_variables(&self, variables: &Map) -> Result<()> { + let rv = unsafe { ffi::vsscript_setVariable(self.handle.as_ptr(), variables.deref()) }; + if rv != 0 { + Err(Error::NoSuchVariable) + } else { + Ok(()) + } + } + + /// Deletes a variable from the script environment. + pub fn clear_variable(&self, name: &str) -> Result<()> { + let name = CString::new(name)?; + let rv = unsafe { ffi::vsscript_clearVariable(self.handle.as_ptr(), name.as_ptr()) }; + if rv != 0 { + Err(Error::NoSuchVariable) + } else { + Ok(()) + } + } +} diff --git a/vapoursynth/src/vsscript/errors.rs b/vapoursynth/src/vsscript/errors.rs new file mode 100644 index 0000000..e91165c --- /dev/null +++ b/vapoursynth/src/vsscript/errors.rs @@ -0,0 +1,60 @@ +use std::ffi::{CString, NulError}; +use std::{fmt, io, result}; + +/// The error type for `vsscript` operations. +#[derive(Fail, Debug)] +pub enum Error { + #[fail(display = "Couldn't convert to a CString")] + CStringConversion(#[cause] NulError), + #[fail(display = "Couldn't open the file")] + FileOpen(#[cause] io::Error), + #[fail(display = "Couldn't read the file")] + FileRead(#[cause] io::Error), + #[fail(display = "Path isn't valid Unicode")] + PathInvalidUnicode, + #[fail(display = "An error occurred in VSScript")] + VSScript(#[cause] VSScriptError), + #[fail(display = "There's no such variable")] + NoSuchVariable, + #[fail(display = "Couldn't get the core")] + NoCore, + #[fail(display = "There's no output on the requested index")] + NoOutput, + #[fail(display = "Couldn't get the VapourSynth API")] + NoAPI, +} + +impl From for Error { + #[inline] + fn from(x: NulError) -> Self { + Error::CStringConversion(x) + } +} + +impl From for Error { + #[inline] + fn from(x: VSScriptError) -> Self { + Error::VSScript(x) + } +} + +pub(crate) type Result = result::Result; + +/// A container for a VSScript error. +#[derive(Fail, Debug)] +pub struct VSScriptError(CString); + +impl fmt::Display for VSScriptError { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_string_lossy()) + } +} + +impl VSScriptError { + /// Creates a new `VSScriptError` with the given error message. + #[inline] + pub(crate) fn new(message: CString) -> Self { + VSScriptError(message) + } +} diff --git a/vapoursynth/src/vsscript/mod.rs b/vapoursynth/src/vsscript/mod.rs new file mode 100644 index 0000000..b483b0b --- /dev/null +++ b/vapoursynth/src/vsscript/mod.rs @@ -0,0 +1,69 @@ +//! VapourSynth script-related things. + +#[cfg(not(feature = "gte-vsscript-api-32"))] +use std::sync::Mutex; +use std::sync::{Once, ONCE_INIT}; +use vapoursynth_sys as ffi; + +#[cfg(not(feature = "gte-vsscript-api-32"))] +lazy_static! { + static ref FFI_CALL_MUTEX: Mutex<()> = Mutex::new(()); +} + +// Some `vsscript_*` function calls have threading issues. Protect them with a mutex. +// https://github.com/vapoursynth/vapoursynth/issues/367 +macro_rules! call_vsscript { + ($call:expr) => {{ + // Fixed in VSScript API 3.2. + // TODO: also not needed when we're running API 3.2 even without a feature. + #[cfg(not(feature = "gte-vsscript-api-32"))] + let _lock = FFI_CALL_MUTEX.lock(); + + $call + }}; +} + +/// Ensures `vsscript_init()` has been called at least once. +// TODO: `vsscript_init()` is already thread-safe with `std::call_once()`, maybe this can be done +// differently to remove the thread protection on Rust's side? An idea is to have a special type +// which calls `vsscript_init()` in `new()` and `vsscript_finalize()` in `drop()` and have the rest +// of the API accessible through that type, however that could become somewhat unergonomic with +// having to store its lifetime everywhere and potentially pass it around the threads. +#[inline] +pub(crate) fn maybe_initialize() { + static ONCE: Once = ONCE_INIT; + + ONCE.call_once(|| unsafe { + ffi::vsscript_init(); + + // Verify the VSScript API version. + #[cfg(feature = "gte-vsscript-api-31")] + { + fn split_version(version: i32) -> (i32, i32) { + (version >> 16, version & 0xFFFF) + } + + let vsscript_version = ffi::vsscript_getApiVersion(); + let (major, minor) = split_version(vsscript_version); + let (my_major, my_minor) = split_version(ffi::VSSCRIPT_API_VERSION); + + if my_major != major { + panic!( + "Invalid VSScript major API version (expected: {}, got: {})", + my_major, major + ); + } else if my_minor > minor { + panic!( + "Invalid VSScript minor API version (expected: >= {}, got: {})", + my_minor, minor + ); + } + } + }); +} + +mod errors; +pub use self::errors::{Error, VSScriptError}; + +mod environment; +pub use self::environment::{Environment, EvalFlags}; diff --git a/vapoursynth/test-vpy/alpha.vpy b/vapoursynth/test-vpy/alpha.vpy new file mode 100644 index 0000000..d819929 --- /dev/null +++ b/vapoursynth/test-vpy/alpha.vpy @@ -0,0 +1,17 @@ +import vapoursynth as vs +from vapoursynth import core +video = core.std.BlankClip(width = 1920, + height = 1080, + format = vs.RGB24, + length = 100, + fpsnum = 60, + fpsden = 1, + color = [0, 255, 0]) +alpha = core.std.BlankClip(width = 1920, + height = 1080, + format = vs.GRAY8, + length = 100, + fpsnum = 60, + fpsden = 1, + color = [128]) +video.set_output(alpha = alpha) diff --git a/vapoursynth/test-vpy/gradient.vpy b/vapoursynth/test-vpy/gradient.vpy new file mode 100644 index 0000000..cabf313 --- /dev/null +++ b/vapoursynth/test-vpy/gradient.vpy @@ -0,0 +1,17 @@ +import vapoursynth as vs +from vapoursynth import core + +def pixel(color): + return core.std.BlankClip(width = 1, + height = 1, + format = vs.RGB24, + length = 1, + color = color) + +def row(red): + return core.std.StackHorizontal( + [pixel([red, green * 16, 0]) for green in range(16)]) + +clip = core.std.StackVertical([row(red * 16) for red in range(16)]) +# clip = core.resize.Lanczos(clip, format = vs.YUV444P8, matrix_s = '709') +clip.set_output() diff --git a/vapoursynth/test-vpy/green.vpy b/vapoursynth/test-vpy/green.vpy new file mode 100644 index 0000000..4a353aa --- /dev/null +++ b/vapoursynth/test-vpy/green.vpy @@ -0,0 +1,11 @@ +import vapoursynth as vs +from vapoursynth import core +video = core.std.BlankClip(width = 1920, + height = 1080, + format = vs.RGB24, + length = 100, + fpsnum = 60, + fpsden = 1, + color = [0, 255, 0]) +# video = core.resize.Bicubic(video, format = vs.YUV444P8, matrix_s = '709') +video.set_output() diff --git a/vapoursynth/test-vpy/pixel-formats.vpy b/vapoursynth/test-vpy/pixel-formats.vpy new file mode 100644 index 0000000..b20d640 --- /dev/null +++ b/vapoursynth/test-vpy/pixel-formats.vpy @@ -0,0 +1,20 @@ +import vapoursynth as vs +from vapoursynth import core + +def make_clip(format, color): + return core.std.BlankClip(width = 320, + height = 240, + format = format, + length = 100, + fpsnum = 60, + fpsden = 1, + color = color) + +make_clip(vs.YUV420P10, [789, 123, 456]).set_output(0) +make_clip(vs.YUV444PS, [5.0, 42.0, 0.25]).set_output(1) +make_clip(vs.RGBS, [0.125, 10.0, 0.5]).set_output(2) +format = core.register_format(vs.YUV, vs.INTEGER, 17, 0, 0) +make_clip(format.id, [77777, 88888, 99999]).set_output(3) +format = core.register_format(vs.YUV, vs.INTEGER, 32, 0, 0) +make_clip(format.id, [2**32-1, 12345, 65432]).set_output(4) +make_clip(vs.RGBH, [0.0625, 5.0, 0.25]).set_output(5) diff --git a/vapoursynth/test-vpy/variable.vpy b/vapoursynth/test-vpy/variable.vpy new file mode 100644 index 0000000..aed61af --- /dev/null +++ b/vapoursynth/test-vpy/variable.vpy @@ -0,0 +1,18 @@ +import vapoursynth as vs +from vapoursynth import core +video = core.std.BlankClip(width = 1920, + height = 1080, + format = vs.RGB24, + length = 100, + fpsnum = 60, + fpsden = 1, + color = [0, 255, 0]) +video2 = core.std.BlankClip(width = 1280, + height = 720, + format = vs.GRAY8, + length = 100, + fpsnum = 30, + fpsden = 1, + color = [127]) +# video = core.resize.Bicubic(video, format = vs.YUV444P8, matrix_s = '709') +core.std.Splice([video, video2], mismatch = True).set_output()