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..4dad2d5 100644 --- a/build.rs +++ b/build.rs @@ -1,16 +1,79 @@ -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=c17").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/ftimage.h") + .clang_arg("-DSTANDALONE_").header("src/raster/ftgrays.h") + .clang_arg("-DFT_BEGIN_HEADER=").clang_arg("-DFT_END_HEADER=") + .clang_arg("-DFT_STATIC_BYTE_CAST(type,var)=(type)(unsigned char)(var)") + .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/ft{misc.h,raster.c}} src/raster/ + * ln -s /path/to/freetype2/src/{raster/ftmisc.h,base/ft{stroke,trigon}.c} src/raster/stroke/ + * ln -s /path/to/freetype2/include/freetype/ft{stroke,trigon,image}.h src/raster/stroke/ + * ln -s /path/to/gpac/src/evg/{ftgrays.c,rast_soft.h,stencil.c,surface.c,\ + raster_{argb,rgb,565,yuv}.c},raster3d.c src/evg/ + * ln -s /path/to/gpac/src/utils/{path2d{,_stroke},math,alloc,color,error}.c src/evg/ + * ln -s /path/gpac/include/gpac/{evg,setup,constants,maths,color,path2d,\ + tools,thread}.h src/evg/gpac/ + * 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").define("GPAC_FIXED_POINT", None) + .flag("-Wno-unused-parameter").define("GPAC_DISABLE_THREADS", None) + .flag("-Wno-pointer-sign").define("GPAC_DISABLE_LOG", None) + .files(glob::glob("src/evg/*.c")?.filter_map(Result::ok)) + .opt_level(3).define("NDEBUG", None).compile(module); + + bindgen::Builder::default().header("src/evg/gpac/evg.h").clang_arg("-Isrc/evg") + .default_enum_style(bindgen::EnumVariation::Rust { non_exhaustive: true }) + .clang_arg("-DGPAC_DISABLE_THREADS").allowlist_function("gf_evg_s.*") + //.allowlist_item("GF_LINE_.*") + .clang_arg("-DGPAC_FIXED_POINT").allowlist_function("gf_path_.*") + .layout_tests(false).derive_copy(false).new_type_alias("Fixed") + .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..fbf306c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,26 @@ +#![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")); + + impl Copy for Fixed {} + impl Clone for Fixed { #[inline] fn clone(&self) -> Self { *self } } + impl From for Fixed { #[inline] fn from(v: i32) -> Self { Self(v << 16) } } + impl From for i32 { #[inline] fn from(v: Fixed) -> Self { (v.0 + (1 << 15)) >> 16 } } + impl From for Fixed { #[inline] fn from(v: f32) -> Self { // 16.16 fixed-point + Self((v * (1 << 16) as f32) as i32) } } + impl From for f32 { #[inline] fn from(v: Fixed) -> Self { + v.0 as f32 / (1 << 16) as f32 } } +} + 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..157edd0 --- /dev/null +++ b/tests/raster.rs @@ -0,0 +1,158 @@ + +#[cfg(feature = "cc")] +#[test] fn raster_evg() -> Result<(), Box> { + use intvg::gpac_evg::*; + + // /*#[macro_export] */macro_rules! dbg_ne { ($($v:expr),*) => () } + macro_rules! dbg_ne { ($v:expr,$g:expr) => { if $v != $g { dbg!($v); } } } + + let (width, height) = (1024_i32, 512_i32); + //let mut bbox = GF_IRect { x: 0, y: 0, width, height }; + let mut pixels = vec![0u8; (width * height * 4) as usize]; + + unsafe { + let surf = gf_evg_surface_new(Bool::GF_FALSE); + let _err = gf_evg_surface_attach_to_buffer(surf, pixels.as_mut_ptr(), + width as u32, height as u32, 4, width << 2, GF_PixelFormat::GF_PIXEL_RGBA); + dbg_ne!(_err, GF_Err::GF_OK); + //let _err = gf_evg_surface_clear(surf, &mut bbox, 0xFF000000); + //dbg_ne!(_err, GF_Err::GF_OK); + + let path = gf_path_new(); + + //gf_path_add_move_to(path, x, y); + //gf_path_add_line_to(path, x, y); + //gf_path_add_cubic_to(path, c1_x, c1_y, c2_x, c2_y, x, y); + //gf_path_add_svg_arc_to(path, end_x, end_y, r_x, r_y, x_rotation, large_arc, sweep); + //gf_path_add_quadratic_to(path, c_x, c_y, x, y); + //gf_path_add_bezier(path, pts, nb_pts); + + //let _err = gf_path_close(path); dbg_ne!(_err, GF_Err::GF_OK); + + let (rw, rh) = ((width >> 1).into(), (height >> 1).into()); + //gf_path_add_ellipse(path, cx, cy, a_axis, b_axis); + //gf_path_add_arc(path, radius, start_angle, end_angle, close_type); + gf_path_add_rect_center(path, rw, rh, rw, rh); + + let stencil = gf_evg_stencil_new(GF_StencilType::GF_STENCIL_SOLID); + + let _err = gf_evg_stencil_set_brush_color(stencil, 0xFF0000FF); + //let _err = gf_evg_stencil_set_linear_gradient(stencil, start_x, start_y, end_x, end_y); + //let _err = gf_evg_stencil_set_radial_gradient(stencil, + // cx, cy, fx, fy, x_radius, y_radius); + dbg_ne!(_err, GF_Err::GF_OK); + + //let _err = gf_evg_stencil_set_gradient_interpolation(stencil, pos, col, count); + //let _err = gf_evg_stencil_set_gradient_mode(stencil, + // GF_GradientMode::GF_GRADIENT_MODE_PAD); + //let _err = gf_evg_stencil_set_alpha(stencil, alpha); + + let _err = gf_evg_surface_set_path(surf, path); + dbg_ne!(_err, GF_Err::GF_OK); + let _err = gf_evg_surface_fill(surf, stencil); + dbg_ne!(_err, GF_Err::GF_OK); + + let pen = GF_PenSettings { + width: 10.into(), cap: 1, join: 1, align: 0, dash: 0, + // XXX: GF_LINE_(CAP/JOIN)_ROUND, GF_PATH_LINE_CENTER, GF_DASH_STYLE_PLAIN + dash_offset: 0.into(), dash_set: core::ptr::null_mut(), + path_length: 0.into(), miterLimit: 0.into(), + }; + let gpol = gf_path_get_outline(path, pen); // XXX: why not closed? + + let _err = gf_evg_stencil_set_brush_color(stencil, 0xAA00FF00); + dbg_ne!(_err, GF_Err::GF_OK); + + let _err = gf_evg_surface_set_path(surf, gpol); + dbg_ne!(_err, GF_Err::GF_OK); + let _err = gf_evg_surface_fill(surf, stencil); + dbg_ne!(_err, GF_Err::GF_OK); + + gf_path_del(gpol); + gf_path_del(path); + gf_evg_surface_delete(surf); + gf_evg_stencil_delete(stencil); + } + + let bufw = std::io::BufWriter::new( //env!("OUT_DIR") + std::fs::File::create(std::path::Path::new(concat!("target", "/image.png")))?); + let mut encoder = png::Encoder::new(bufw, + width as u32, height as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); + // png::ScaledFloat::from_scaled(45455) // 1.0 / 2.2 scaled by 100000 + //let source_chromaticities = png::SourceChromaticities::new( // unscaled instantiation + // (0.31270, 0.32900), (0.64000, 0.33000), (0.30000, 0.60000), (0.15000, 0.06000)); + //encoder.set_source_chromaticities(source_chromaticities); + encoder.write_header()?.write_image_data(&pixels)?; + + Ok(()) +} + +#[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(()) +}