diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3ef992a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "C_Cpp.default.defines": [ "STANDALONE_", + "FALL_THROUGH=((void)0)", "FT_BEGIN_HEADER" ], + "C_Cpp.default.includePath": [ "src/evg", "src/raster" ] +} diff --git a/Cargo.toml b/Cargo.toml index fe4e828..5d56575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,30 @@ usvg = "0.36" kurbo = "0.10" # Bezier curves utils #build-time = { version = "0.1", git = "https://github.com/AlephAlpha/build-time" } +png = { version = "0.17", optional = true } + +[lib] +# Disable doctests as a workaround for https://github.com/rust-lang/rust-bindgen/issues/1313 +#doctest = false + +[features] +#default = [ "cc" ] +cc = [ "dep:cc", "dep:bindgen", "dep:glob", "dep:png" ] # implied by optional dependency [dev-dependencies] #rexpect = "0.5" [build-dependencies] +cc = { version = "1.0", optional = true } +bindgen = { version = "0.68", optional = true } +glob = { version = "0.3", optional = true } chrono = "0.4" +[profile.release] +codegen-units = 1 +strip = 'debuginfo' +panic = 'abort' +lto = 'fat' # true + [workspace] diff --git a/build.rs b/build.rs index 8ad21b6..e4b8bfc 100644 --- a/build.rs +++ b/build.rs @@ -1,16 +1,72 @@ -fn main() { // https://doc.rust-lang.org/stable/cargo/reference/build-scripts.html +// https://doc.rust-lang.org/stable/cargo/reference/build-scripts.html +fn main() -> Result<(), Box> { + //println!("cargo:rerun-if-changed=build.rs"); // XXX: prevent re-run indead // By default, cargo always re-run the build script if any file within the package // is changed, and no any rerun-if instruction is emitted. println!("cargo:rerun-if-changed=src/"); println!("cargo:rerun-if-changed=.git/index"); let output = std::process::Command::new("git") - .args(["rev-parse", "--short", "HEAD"]).output().unwrap(); - println!("cargo:rustc-env=BUILD_GIT_HASH={}", String::from_utf8(output.stdout).unwrap()); + .args(["rev-parse", "--short", "HEAD"]).output()?; + println!("cargo:rustc-env=BUILD_GIT_HASH={}", String::from_utf8(output.stdout)?); println!("cargo:rustc-env=BUILD_TIMESTAMP={}", chrono::Local::now().format("%H:%M:%S%z %Y-%m-%d")); - //println!("cargo:rerun-if-changed=build.rs"); // XXX: prevent re-run indead + #[cfg(feature = "cc")] { use std::path::PathBuf; + #[derive(Debug)] struct DoctestComment; + impl bindgen::callbacks::ParseCallbacks for DoctestComment { + fn process_comment(&self, comment: &str) -> Option { + Some(format!("````c,ignore\n{comment}\n````")) // FIXME: + } + } + + let module = "ftgrays"; + cc::Build::new().flag("-std=c99").flag("-pedantic") + .define("FALL_THROUGH", "((void)0)").file("src/raster/ftgrays.c") + .opt_level(3).define("NDEBUG", None).file("src/raster/ftraster.c") + .files(glob::glob("src/raster/stroke/*.c")?.filter_map(Result::ok)) + //.flag("-Wno-unused-variable").flag("-Wno-unused-function") + .define("STANDALONE_", None).compile(module); + + // The bindgen::Builder is the main entry point to bindgen, + // and lets you build up options for the resulting bindings. + bindgen::Builder::default().header("src/raster/ftmisc.h") + .clang_arg(r"-DFT_CONFIG_STANDARD_LIBRARY_H=") + .clang_arg("-DSTANDALONE_").header("src/raster/ftgrays.h") + .clang_arg("-DFT_BEGIN_HEADER=").clang_arg("-DFT_END_HEADER=") + .allowlist_item("FT_OUTLINE_.*|FT_RASTER_FLAG_.*|FT_CURVE_TAG.*") + .layout_tests(false).derive_copy(false).allowlist_var("ft_standard_raster") + .allowlist_var("ft_grays_raster").allowlist_type("FT_Outline|FT_Pixel_Mode") + .default_enum_style(bindgen::EnumVariation::Rust { non_exhaustive: true }) + .default_macro_constant_type(bindgen::MacroTypeVariation::Signed) + .parse_callbacks(Box::new(DoctestComment)).generate_comments(false) // XXX: + // Tell cargo to invalidate the built crate whenever any of the + // included header files changed. + .parse_callbacks(Box::new(bindgen::CargoCallbacks)).generate()? + .write_to_file(PathBuf::from(std::env::var("OUT_DIR")?).join(format!("{module}.rs")))?; + + /* ln -s /path/to/freetype2/{include/freetype/ftimage.h,src/smooth/ftgrays.[ch],\ + src/raster/{ftmisc.h,ftraster.c}} src/raster/ # ftstroke.c,fttrigon.c + * ln -s /path/to/gpac/src/evg/{ftgrays.c,rast_soft.h,stencil.c,surface.c,\ + raster_{argb,rgb,565}.c} src/evg/ + * ln -s /path/to/gpac/include/gpac/{evg,color,constants,maths,path2d,setup,\ + tools,thread}.h src/evg/ + * touch src/evg/gpac/{Remotery,config_file,configuration,main,module,version}.h + */ + let module = "gpac_evg"; + cc::Build::new().flag("-std=c17").flag("-Isrc/evg") + .flag("-Wno-unused-parameter").define("GPAC_DISABLE_THREADS", None) + .files(glob::glob("src/evg/*.c")?.filter_map(Result::ok)) + .flag("-Wno-pointer-sign").opt_level(3).define("NDEBUG", None).compile(module); + + bindgen::Builder::default().header("src/evg/gpac/evg.h").clang_arg("-Isrc/evg") + .layout_tests(false).derive_copy(false).allowlist_function("gf_evg_surface_.*") + .allowlist_function("gf_path_get_outline") // for stroke path + .parse_callbacks(Box::new(bindgen::CargoCallbacks)).generate()? + .write_to_file(PathBuf::from(std::env::var("OUT_DIR")?).join(format!("{module}.rs")))?; + } + + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 7e19ac2..e50651e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,15 @@ +#![allow(non_snake_case)] #![allow(non_camel_case_types)] #![allow(non_upper_case_globals)] +#![allow(clippy::approx_constant)] #![allow(clippy::useless_transmute)] +#[cfg(feature = "cc")] pub mod ftgrays { + include!(concat!(env!("OUT_DIR"), "/ftgrays.rs")); + + impl From<(FT_Pos, FT_Pos)> for FT_Vector { + fn from(v: (FT_Pos, FT_Pos)) -> Self { Self { x: v.0, y: v.1 } } + } +} +#[cfg(feature = "cc")] pub mod gpac_evg { include!(concat!(env!("OUT_DIR"), "/gpac_evg.rs")); } + pub mod tinyvg; pub mod render; pub mod convert; diff --git a/src/render.rs b/src/render.rs index 0397229..cb4bbdb 100644 --- a/src/render.rs +++ b/src/render.rs @@ -3,6 +3,13 @@ use crate::tinyvg::*; use tiny_skia as skia; use std::result::Result; +/* TODO: https://github.com/sammycage/plutovg + https://github.com/gpac/gpac/tree/master/src/evg/ + https://github.com/kiba/SDL_svg/blob/master/ftgrays.c + https://github.com/Samsung/rlottie/tree/master/src/vector/freetype + https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/src/smooth/ftgrays.c + https://gitlab.freedesktop.org/freetype/freetype/-/blob/master/src/raster/ftraster.c */ + pub trait Render { fn render(&self, scale: f32) -> Result; } impl Render for TVGImage { // TODO: to render with femtovg? diff --git a/tests/raster.rs b/tests/raster.rs new file mode 100644 index 0000000..2d7eaf9 --- /dev/null +++ b/tests/raster.rs @@ -0,0 +1,67 @@ + +#[cfg(feature = "cc")] +#[test] fn raster_ftgrays() -> Result<(), Box> { + use core::{ptr::null_mut, ffi::c_void}; use intvg::ftgrays::*; + + let mut points = vec![ // FTDemo_Icon @freetype2/demo/src/ftcommon.c + ( 4, 8).into(), ( 4,10).into(), ( 8,12).into(), ( 8,52).into(), ( 4,54).into(), + ( 4,56).into(), (60,56).into(), (60,44).into(), (58,44).into(), (56,52).into(), + (44,52).into(), (44,12).into(), (48,10).into(), (48, 8).into(), (32, 8).into(), + (32,10).into(), (36,12).into(), (36,52).into(), (16,52).into(), (16,36).into(), + (24,36).into(), (26,40).into(), (28,40).into(), (28,28).into(), (26,28).into(), + (24,32).into(), (16,32).into(), (16,12).into(), (20,10).into(), (20, 8).into(), ]; + let mut tags = vec![FT_CURVE_TAG_ON as i8; points.len()]; + let mut contours = vec![29]; + + let mut bitmap = FT_Bitmap { + rows: 0, width: 0, pitch: 0, buffer: null_mut(), // FIXME: + + pixel_mode: FT_Pixel_Mode::FT_PIXEL_MODE_LCD as u8, // FT_PIXEL_MODE_BGRA + // https://docs.rs/bindgen/latest/bindgen/struct.Builder.html#enums + // Not used currently, or only used with FT_PIXEL_MODE_GRAY + palette_mode: 0, palette: null_mut(), num_grays: 0, + }; + + #[allow(unused)] + unsafe extern "C" fn span_func(y: i32, count: i32, // TODO: solid/linear/radial/stroke + spans: *const FT_Span, user: *mut c_void) { // The gray span drawing callback. + let bitmap = user as *mut FT_Bitmap; assert!(0 < count); + + for span in core::slice::from_raw_parts(spans, count as usize) { + for x in span.x..span.len as i16 { + // TODO: handle pixel at (x, y) with span.coverage (alpha) + } + } + } + + let outline = FT_Outline { + n_points: points.len() as i16, points: points.as_mut_ptr(), tags: tags.as_mut_ptr(), + n_contours: contours.len() as i16, contours: contours.as_mut_ptr(), + flags: FT_OUTLINE_NONE, // XXX: FT_OUTLINE_IGNORE_DROPOUTS + }; assert!(tags.len() == points.len()); + + let mut params = FT_Raster_Params { + source: &outline as *const _ as *const c_void, + user: &mut bitmap as *mut _ as *mut c_void, gray_spans: Some(span_func), + + // ftgrays.c: `ft_grays_raster` are typoed as `ft_gray_raster` in some comments. + // ftimage.h: FT_Pos can be typed as `signed int` rather than `long`? // XXX: + clip_box: FT_BBox { xMin: 0, yMin: 0, xMax: 0, yMax: 0 }, // FT_RASTER_FLAG_CLIP + flags: FT_RASTER_FLAG_AA | FT_RASTER_FLAG_DIRECT, + black_spans: None, bit_test: None, bit_set: None, // Unused + target: null_mut(), // &bitmap, unused in direct mode + }; let mut raster = null_mut(); + + unsafe { // use of extern static (ft_grays_raster/ft_standard_raster) is unsafe + let _ = ft_grays_raster.raster_new.unwrap()(null_mut(), &mut raster); + //if res != 0 { panic!("raster_new failed: {}", res); } + //ft_grays_raster.raster_reset.unwrap()(raster, null_mut(), 0); + //ft_grays_raster.raster_set_mode.unwrap()(raster, 0, null_mut()); + + let res = ft_grays_raster.raster_render.unwrap()(raster, &mut params); + if res != 0 { panic!("raster_render failed: {res}"); } + //ft_grays_raster.raster_done.unwrap()(raster); + } + + Ok(()) +}