diff --git a/Cargo.lock b/Cargo.lock index 5a3e632..7179bfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ dependencies = [ "anyhow", "bitvec_helpers", "hevc_parser", + "libc", "serde", "serde_json", ] diff --git a/hdr10plus/Cargo.toml b/hdr10plus/Cargo.toml index 3f61071..4b613a3 100644 --- a/hdr10plus/Cargo.toml +++ b/hdr10plus/Cargo.toml @@ -10,14 +10,34 @@ repository = "https://github.com/quietvoid/hdr10plus_tool/tree/master/hdr10plus" [dependencies] bitvec_helpers = { version = "3.1.4", default-features = false, features = ["bitstream-io"] } +hevc_parser = { version = "0.6.2", optional = true } + anyhow = "1.0.86" serde = { version = "1.0.203", features = ["derive"], optional = true } serde_json = { version = "1.0.117", features = ["preserve_order"], optional = true } -hevc_parser = { version = "0.6.1", optional = true } + +libc = { version = "0.2", optional = true } [features] hevc = ["hevc_parser"] json = ["serde", "serde_json"] +capi = ["libc", "json"] [package.metadata.docs.rs] all-features = true + +[package.metadata.capi.header] +subdirectory = "libhdr10plus-rs" + +[package.metadata.capi.pkg_config] +strip_include_path_components = 1 +subdirectory = false +name = "hdr10plus-rs" +filename = "hdr10plus-rs" + +[package.metadata.capi.library] +rustflags = "-Cpanic=abort" +name = "hdr10plus-rs" + +[lib] +doctest = false diff --git a/hdr10plus/cbindgen.toml b/hdr10plus/cbindgen.toml new file mode 100644 index 0000000..f2611f8 --- /dev/null +++ b/hdr10plus/cbindgen.toml @@ -0,0 +1,15 @@ +header = "// SPDX-License-Identifier: MIT" +sys_includes = ["stddef.h", "stdint.h", "stdlib.h", "stdbool.h"] +no_includes = true +include_guard = "HDR10PLUS_RS_H" +tab_width = 4 +style = "Type" +language = "C" +cpp_compat = true + +[parse] +parse_deps = false + +[export] +item_types = ["constants", "enums", "structs", "unions", "typedefs", "opaque", "functions"] +prefix = "Hdr10PlusRs" diff --git a/hdr10plus/examples/capi_json_file.c b/hdr10plus/examples/capi_json_file.c new file mode 100644 index 0000000..16f79ce --- /dev/null +++ b/hdr10plus/examples/capi_json_file.c @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +#include + +int main(void) { + char *path = "../../assets/hevc_tests/regular_metadata.json"; + int ret; + + Hdr10PlusRsJsonOpaque *hdr10plus_json = hdr10plus_rs_parse_json(path); + const char *error = hdr10plus_rs_json_get_error(hdr10plus_json); + if (error) { + printf("%s\n", error); + + hdr10plus_rs_json_free(hdr10plus_json); + return 1; + } + + const Hdr10PlusRsData *payload = hdr10plus_rs_write_av1_metadata_obu_t35_complete(hdr10plus_json, 0); + if (payload) { + assert(payload->len == 49); + + hdr10plus_rs_data_free(payload); + } + + hdr10plus_rs_json_free(hdr10plus_json); +} diff --git a/hdr10plus/src/c_structs/mod.rs b/hdr10plus/src/c_structs/mod.rs new file mode 100644 index 0000000..dfdfc5b --- /dev/null +++ b/hdr10plus/src/c_structs/mod.rs @@ -0,0 +1,42 @@ +use std::ffi::CString; + +use libc::size_t; + +use crate::metadata_json::MetadataJsonRoot; + +/// Opaque HDR10+ JSON file handle +/// +/// Use `hdr10plus_rs_json_free` to free. +/// It should be freed regardless of whether or not an error occurred. +pub struct JsonOpaque { + /// Optional parsed JSON, present when parsing is successful. + pub metadata_root: Option, + + pub error: Option, +} + +/// Struct representing a data buffer +#[repr(C)] +pub struct Data { + /// Pointer to the data buffer + pub data: *const u8, + /// Data buffer size + pub len: size_t, +} + +impl Data { + /// # Safety + /// The pointers should all be valid. + pub unsafe fn free(&self) { + Vec::from_raw_parts(self.data as *mut u8, self.len, self.len); + } +} + +impl From> for Data { + fn from(buf: Vec) -> Self { + Data { + len: buf.len(), + data: Box::into_raw(buf.into_boxed_slice()) as *const u8, + } + } +} diff --git a/hdr10plus/src/capi.rs b/hdr10plus/src/capi.rs new file mode 100644 index 0000000..1ade48f --- /dev/null +++ b/hdr10plus/src/capi.rs @@ -0,0 +1,141 @@ +#![deny(missing_docs)] + +use anyhow::anyhow; +use libc::{c_char, size_t}; +use std::{ + ffi::{CStr, CString}, + path::PathBuf, + ptr::{null, null_mut}, +}; + +use crate::{ + metadata::{Hdr10PlusMetadata, Hdr10PlusMetadataEncOpts}, + metadata_json::MetadataJsonRoot, +}; + +use super::c_structs::*; + +/// # Safety +/// The pointer to the data must be valid. +/// +/// Parse a HDR10+ JSON file from file path. +/// Adds an error if the parsing fails. +#[no_mangle] +pub unsafe extern "C" fn hdr10plus_rs_parse_json(path: *const c_char) -> *mut JsonOpaque { + if path.is_null() { + return null_mut(); + } + + let mut opaque = JsonOpaque { + metadata_root: None, + error: None, + }; + let mut error = None; + + if let Ok(str) = CStr::from_ptr(path).to_str() { + let path = PathBuf::from(str); + match MetadataJsonRoot::from_file(path) { + Ok(metadata) => opaque.metadata_root = Some(metadata), + Err(e) => { + error = Some(format!( + "hdr10plus_rs_parse_json: Errored while parsing: {e}" + )); + } + }; + } else { + error = + Some("hdr10plus_rs_parse_json: Failed parsing the input path as a string".to_string()); + } + + if let Some(err) = error { + opaque.error = CString::new(err).ok(); + } + + Box::into_raw(Box::new(opaque)) +} + +/// # Safety +/// The pointer to the opaque struct must be valid. +/// +/// Get the last logged error for the JsonOpaque operations. +/// +/// On invalid parsing, an error is added. +/// The user should manually verify if there is an error, as the parsing does not return an error code. +#[no_mangle] +pub unsafe extern "C" fn hdr10plus_rs_json_get_error(ptr: *const JsonOpaque) -> *const c_char { + if ptr.is_null() { + return null(); + } + + let opaque = &*ptr; + + match &opaque.error { + Some(s) => s.as_ptr(), + None => null(), + } +} + +/// # Safety +/// The pointer to the opaque struct must be valid. +/// +/// Free the Hdr10PlusJsonOpaque +#[no_mangle] +pub unsafe extern "C" fn hdr10plus_rs_json_free(ptr: *mut JsonOpaque) { + if !ptr.is_null() { + drop(Box::from_raw(ptr)); + } +} + +/// # Safety +/// The struct pointer must be valid. +/// +/// Writes the encoded HDR10+ payload as a byte buffer, including country code +/// If an error occurs in the writing, returns null +#[no_mangle] +pub unsafe extern "C" fn hdr10plus_rs_write_av1_metadata_obu_t35_complete( + ptr: *mut JsonOpaque, + frame_number: size_t, +) -> *const Data { + if ptr.is_null() { + return null(); + } + + let opaque = &mut *ptr; + let frame_metadata = opaque + .metadata_root + .as_ref() + .and_then(|root| root.scene_info.get(frame_number)) + .ok_or(anyhow!("No metadata for frame {frame_number}")) + .and_then(|jm| { + let enc_opts = Hdr10PlusMetadataEncOpts { + with_country_code: true, + ..Default::default() + }; + + Hdr10PlusMetadata::try_from(jm) + .and_then(|metadata| metadata.encode_with_opts(&enc_opts)) + }); + + match frame_metadata { + Ok(buf) => Box::into_raw(Box::new(Data::from(buf))), + Err(e) => { + opaque + .error + .replace(CString::new(format!("Failed writing byte buffer: {e}")).unwrap()); + + null() + } + } +} + +/// # Safety +/// The data pointer should exist, and be allocated by Rust. +/// +/// Free a Data buffer +#[no_mangle] +pub unsafe extern "C" fn hdr10plus_rs_data_free(data: *const Data) { + if !data.is_null() { + let data = Box::from_raw(data as *mut Data); + data.free(); + } +} diff --git a/hdr10plus/src/lib.rs b/hdr10plus/src/lib.rs index 487fe5e..c56dad6 100644 --- a/hdr10plus/src/lib.rs +++ b/hdr10plus/src/lib.rs @@ -5,3 +5,11 @@ pub mod metadata_json; #[cfg(feature = "hevc")] pub mod hevc; + +/// C API module +#[cfg(any(cargo_c, feature = "capi"))] +pub mod capi; + +/// Structs used and exposed in the C API +#[cfg(any(cargo_c, feature = "capi"))] +pub mod c_structs;