diff --git a/Cargo.toml b/Cargo.toml index 0bfee00..8dd9bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = ["path"] [dependencies] arrayvec = { version = "0.7", default-features = false } -bytemuck = { version = "1.24", features = ["aarch64_simd"] } +bytemuck = { version = "1.24", features = ["aarch64_simd", "derive"] } cfg-if = "1" log = "0.4" png = { version = "0.18", optional = true } @@ -37,3 +37,6 @@ simd = [] # Allows loading and saving `Pixmap` as PNG. png-format = ["std", "dep:png"] + +# Enables 16-bit per channel (RGBA64 / 64bpp) color pipeline support. +16bpc = [] diff --git a/benches/Cargo.lock b/benches/Cargo.lock index 7c8badb..3de4e0b 100644 --- a/benches/Cargo.lock +++ b/benches/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -9,10 +9,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "arrayref" -version = "0.3.7" +name = "adler2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "arrayvec" @@ -37,7 +37,7 @@ name = "benchmarks" version = "0.1.0" dependencies = [ "cairo-rs", - "png", + "png 0.17.8", "raqote", "skia-rs", "tiny-skia", @@ -49,11 +49,31 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "cairo-rs" @@ -61,7 +81,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "157049ba9618aa3a61c39d5d785102c04d3b1f40632a706c621a9aedc21e6084" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "libc", ] @@ -108,21 +128,22 @@ dependencies = [ [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] [[package]] name = "flate2" -version = "1.0.26" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.0", + "zlib-rs", ] [[package]] @@ -158,6 +179,26 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5faa9f23e86bd5768d76def086192ff5f869fb088da12a976ea21e9796b975f6" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -179,11 +220,42 @@ version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaeebc51f9e7d2c150d3f3bfeb667f2aa985db5ef1e3d212847bdedb488beeaa" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", ] [[package]] @@ -223,24 +295,33 @@ version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac8fb7895b4afa060ad731a32860db8755da3449a47e796d5ecf758db2671d4" +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tiny-skia" -version = "0.11.1" +version = "0.12.0" dependencies = [ - "arrayref", "arrayvec 0.7.2", "bytemuck", "cfg-if", "log", - "png", + "png 0.18.1", "tiny-skia-path", ] [[package]] name = "tiny-skia-path" -version = "0.11.1" +version = "0.12.0" dependencies = [ - "arrayref", "bytemuck", "strict-num", ] @@ -250,3 +331,15 @@ name = "typed-arena" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" diff --git a/benches/src/blend.rs b/benches/src/blend.rs index 87f3cdc..763e89d 100644 --- a/benches/src/blend.rs +++ b/benches/src/blend.rs @@ -33,6 +33,17 @@ fn fill_tiny_skia(blend_mode: tiny_skia::BlendMode, bencher: &mut Bencher) { pb.finish().unwrap() }; + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + pixmap.fill_path(&path1, &paint1, FillRule::Winding, Transform::identity(), None); + + bencher.iter(|| { + pixmap.fill_path(&path2, &paint2, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); pixmap.fill_path(&path1, &paint1, FillRule::Winding, Transform::identity(), None); diff --git a/benches/src/clip.rs b/benches/src/clip.rs index 53b413f..00c6c80 100644 --- a/benches/src/clip.rs +++ b/benches/src/clip.rs @@ -9,6 +9,27 @@ fn do_clip_tiny_skia(aa: bool, bencher: &mut Bencher) { let path = PathBuilder::from_rect(Rect::from_xywh(0.0, 0.0, 1000.0, 1000.0).unwrap()); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + let clip_path = { + let mut pb = PathBuilder::new(); + pb.push_rect(Rect::from_xywh(100.0, 100.0, 800.0, 800.0).unwrap()); + pb.push_rect(Rect::from_xywh(300.0, 300.0, 400.0, 400.0).unwrap()); + pb.finish().unwrap() + }; + + let clip_path = clip_path.transform(Transform::from_row(1.0, -0.5, 0.0, 1.0, 0.0, 300.0)).unwrap(); + + let mut mask = Mask::new(1000, 1000).unwrap(); + mask.fill_path(&clip_path, FillRule::EvenOdd, aa, Transform::identity()); + + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), Some(&mask)); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { let clip_path = { diff --git a/benches/src/draw_pixmap.rs b/benches/src/draw_pixmap.rs new file mode 100644 index 0000000..54e8b8c --- /dev/null +++ b/benches/src/draw_pixmap.rs @@ -0,0 +1,49 @@ +use test::Bencher; + +fn bench_draw_pixmap(blend_mode: tiny_skia::BlendMode, bencher: &mut Bencher) { + use tiny_skia::*; + + let is_16bpc = crate::is_16bpc(); + + #[cfg(feature = "16bpc")] + if is_16bpc { + let mut dst = PixmapU16::new(1000, 1000).unwrap(); + let mut src = PixmapU16::new(800, 800).unwrap(); + src.fill(Color::from_rgba8(160, 80, 40, 200)); + + let paint = PixmapPaint { + opacity: 0.85, + blend_mode, + quality: FilterQuality::Nearest, + }; + + bencher.iter(|| { + dst.draw_pixmap(100, 100, src.as_ref(), &paint, Transform::identity(), None); + }); + return; + } + + let mut dst = Pixmap::new(1000, 1000).unwrap(); + let mut src = Pixmap::new(800, 800).unwrap(); + src.fill(Color::from_rgba8(160, 80, 40, 200)); + + let paint = PixmapPaint { + opacity: 0.85, + blend_mode, + quality: FilterQuality::Nearest, + }; + + bencher.iter(|| { + dst.draw_pixmap(100, 100, src.as_ref(), &paint, Transform::identity(), None); + }); +} + +#[bench] fn draw_pixmap_source_over(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::SourceOver, bencher); } +#[bench] fn draw_pixmap_multiply(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::Multiply, bencher); } +#[bench] fn draw_pixmap_screen(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::Screen, bencher); } +#[bench] fn draw_pixmap_overlay(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::Overlay, bencher); } +#[bench] fn draw_pixmap_color_dodge(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::ColorDodge, bencher); } +#[bench] fn draw_pixmap_color_burn(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::ColorBurn, bencher); } +#[bench] fn draw_pixmap_soft_light(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::SoftLight, bencher); } +#[bench] fn draw_pixmap_saturation(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::Saturation, bencher); } +#[bench] fn draw_pixmap_color(bencher: &mut Bencher) { bench_draw_pixmap(tiny_skia::BlendMode::Color, bencher); } diff --git a/benches/src/fill.rs b/benches/src/fill.rs index 8b9595b..7dd4ed5 100644 --- a/benches/src/fill.rs +++ b/benches/src/fill.rs @@ -10,6 +10,15 @@ fn rect_tiny_skia(bencher: &mut Bencher) { let rect = Rect::from_xywh(50.7, 20.1, 812.4, 777.3).unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_rect(rect, &paint, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -79,6 +88,24 @@ fn rect_aa_tiny_skia(bencher: &mut Bencher) { let rect = Rect::from_xywh(50.7, 20.1, 812.4, 777.3).unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_rect(rect, &paint, Transform::identity(), None); + }); + return; + } + + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_rect(rect, &paint, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -150,10 +177,18 @@ fn rect_aa_ts_tiny_skia(bencher: &mut Bencher) { let rect = Rect::from_xywh(200.3, 100.4, 500.5, 300.2).unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_rect(rect, &paint, Transform::from_row(1.8, 0.3, -0.7, 0.8, 12.0, 15.3), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { - pixmap.fill_rect(rect, &paint, Transform::from_row(1.8, 0.3, -0.7, 0.8, 12.0, 15.3), None); }); } @@ -221,8 +256,18 @@ fn rect_aa_ts_cairo(bencher: &mut Bencher) { fn all_tiny_skia(bencher: &mut Bencher) { use tiny_skia::*; - let mut pixmap = Pixmap::new(1000, 1000).unwrap(); let c = Color::from_rgba8(50, 100, 150, 200); + + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill(c); + }); + return; + } + + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { pixmap.fill(c); }); @@ -286,6 +331,15 @@ fn path_aa_tiny_skia(bencher: &mut Bencher) { pb.close(); let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::EvenOdd, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -391,6 +445,15 @@ fn source_tiny_skia(bencher: &mut Bencher) { let path = PathBuilder::from_rect(Rect::from_ltrb(100.0, 100.0, 900.0, 900.0).unwrap()); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -479,6 +542,15 @@ fn opaque_tiny_skia(bencher: &mut Bencher) { let path = PathBuilder::from_rect(Rect::from_ltrb(100.0, 100.0, 900.0, 900.0).unwrap()); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { diff --git a/benches/src/gradients.rs b/benches/src/gradients.rs index fa8e1d9..51a2fff 100644 --- a/benches/src/gradients.rs +++ b/benches/src/gradients.rs @@ -27,6 +27,15 @@ fn two_stops_linear_tiny_skia( pb.close(); let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -557,6 +566,15 @@ fn simple_radial_tiny_skia(bencher: &mut Bencher) { pb.close(); let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { @@ -712,6 +730,15 @@ fn two_point_radial_tiny_skia(bencher: &mut Bencher) { pb.close(); let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { diff --git a/benches/src/hairline.rs b/benches/src/hairline.rs index 2b317ac..90a908a 100644 --- a/benches/src/hairline.rs +++ b/benches/src/hairline.rs @@ -220,6 +220,15 @@ fn draw_tiny_skia(aa: bool, bencher: &mut Bencher) { let mut stroke = Stroke::default(); stroke.width = 0.5; + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { diff --git a/benches/src/main.rs b/benches/src/main.rs index a1a855a..672c812 100644 --- a/benches/src/main.rs +++ b/benches/src/main.rs @@ -2,6 +2,10 @@ extern crate test; +pub fn is_16bpc() -> bool { + std::env::var("TINY_SKIA_BENCH_16BPC").is_ok() +} + #[rustfmt::skip] #[cfg(test)] mod blend; @@ -10,6 +14,9 @@ mod blend; mod clip; #[rustfmt::skip] #[cfg(test)] +mod draw_pixmap; +#[rustfmt::skip] +#[cfg(test)] mod fill; #[rustfmt::skip] #[cfg(test)] diff --git a/benches/src/patterns.rs b/benches/src/patterns.rs index 3a4dd70..8c7173f 100644 --- a/benches/src/patterns.rs +++ b/benches/src/patterns.rs @@ -24,7 +24,6 @@ fn pattern_tiny_skia( pixmap } - let mut pixmap = Pixmap::new(1000, 1000).unwrap(); let triangle = crate_triangle(); let mut paint = Paint::default(); @@ -45,6 +44,17 @@ fn pattern_tiny_skia( pb.close(); let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + }); + return; + } + + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); + bencher.iter(|| { pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); }); diff --git a/benches/src/spiral.rs b/benches/src/spiral.rs index cbccda5..994682a 100644 --- a/benches/src/spiral.rs +++ b/benches/src/spiral.rs @@ -218,6 +218,15 @@ fn tiny_skia(bencher: &mut Bencher) { } let path = pb.finish().unwrap(); + #[cfg(feature = "16bpc")] + if crate::is_16bpc() { + let mut pixmap = PixmapU16::new(1000, 1000).unwrap(); + bencher.iter(|| { + pixmap.stroke_path(&path, &paint, &Stroke::default(), Transform::identity(), None); + }); + return; + } + let mut pixmap = Pixmap::new(1000, 1000).unwrap(); bencher.iter(|| { pixmap.stroke_path(&path, &paint, &Stroke::default(), Transform::identity(), None); diff --git a/src/color.rs b/src/color.rs index 07af604..3783eb3 100644 --- a/src/color.rs +++ b/src/color.rs @@ -19,6 +19,18 @@ pub const ALPHA_U8_TRANSPARENT: AlphaU8 = 0x00; /// Represents fully opaque AlphaU8 value. pub const ALPHA_U8_OPAQUE: AlphaU8 = 0xFF; +/// 16-bit type for an alpha value. 65535 is 100% opaque, zero is 100% transparent. +#[cfg(feature = "16bpc")] +pub type AlphaU16 = u16; + +/// Represents fully transparent AlphaU16 value. +#[cfg(feature = "16bpc")] +pub const ALPHA_U16_TRANSPARENT: AlphaU16 = 0x0000; + +/// Represents fully opaque AlphaU16 value. +#[cfg(feature = "16bpc")] +pub const ALPHA_U16_OPAQUE: AlphaU16 = 0xFFFF; + /// Represents fully transparent Alpha value. pub const ALPHA_TRANSPARENT: NormalizedF32 = NormalizedF32::ZERO; @@ -29,7 +41,7 @@ pub const ALPHA_OPAQUE: NormalizedF32 = NormalizedF32::ONE; /// /// Byteorder: RGBA (relevant for bytemuck casts) #[repr(transparent)] -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] pub struct ColorU8([u8; 4]); impl ColorU8 { @@ -79,6 +91,12 @@ impl ColorU8 { PremultipliedColorU8::from_rgba_unchecked(self.red(), self.green(), self.blue(), a) } } + + /// Converts into a 16bpc `ColorU16` via bit replication `(c << 8) | c`. + #[cfg(feature = "16bpc")] + pub fn to_color_u16(self) -> ColorU16 { + ColorU16::from_color_u8(self) + } } impl core::fmt::Debug for ColorU8 { @@ -92,17 +110,105 @@ impl core::fmt::Debug for ColorU8 { } } +/// A 64-bit RGBA color value with 16 bits per channel (unmultiplied). +#[cfg(feature = "16bpc")] +#[repr(transparent)] +#[derive(Copy, Clone, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct ColorU16([u16; 4]); + +#[cfg(feature = "16bpc")] +impl ColorU16 { + /// Creates a new color. + pub const fn from_rgba(r: u16, g: u16, b: u16, a: u16) -> Self { + ColorU16([r, g, b, a]) + } + + /// Returns color's red component. + pub const fn red(self) -> u16 { + self.0[0] + } + + /// Returns color's green component. + pub const fn green(self) -> u16 { + self.0[1] + } + + /// Returns color's blue component. + pub const fn blue(self) -> u16 { + self.0[2] + } + + /// Returns color's alpha component. + pub const fn alpha(self) -> u16 { + self.0[3] + } + + /// Check that color is opaque. + /// + /// Alpha == 65535 + pub fn is_opaque(&self) -> bool { + self.alpha() == ALPHA_U16_OPAQUE + } + + /// Converts from 8-bit ColorU8 via bit replication `(c << 8) | c`. + pub fn from_color_u8(c: ColorU8) -> Self { + #[inline(always)] + fn up(v: u8) -> u16 { + let v = v as u16; + (v << 8) | v + } + ColorU16([up(c.red()), up(c.green()), up(c.blue()), up(c.alpha())]) + } + + /// Converts to 8-bit ColorU8 via `(c + 128) >> 8`. + pub fn to_color_u8(self) -> ColorU8 { + #[inline(always)] + fn down(v: u16) -> u8 { + ((v as u32 + 128) >> 8).min(255) as u8 + } + ColorU8::from_rgba( + down(self.red()), + down(self.green()), + down(self.blue()), + down(self.alpha()), + ) + } + + /// Converts into a premultiplied color. + pub fn premultiply(&self) -> PremultipliedColorU16 { + let a = self.alpha(); + if a != ALPHA_U16_OPAQUE { + PremultipliedColorU16::from_rgba_unchecked( + premultiply_u16(self.red(), a), + premultiply_u16(self.green(), a), + premultiply_u16(self.blue(), a), + a, + ) + } else { + PremultipliedColorU16::from_rgba_unchecked(self.red(), self.green(), self.blue(), a) + } + } +} + +#[cfg(feature = "16bpc")] +impl core::fmt::Debug for ColorU16 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ColorU16") + .field("r", &self.red()) + .field("g", &self.green()) + .field("b", &self.blue()) + .field("a", &self.alpha()) + .finish() + } +} + /// A 32-bit premultiplied RGBA color value. /// /// Byteorder: RGBA (relevant for bytemuck casts) #[repr(transparent)] -#[derive(Copy, Clone, PartialEq)] +#[derive(Copy, Clone, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] pub struct PremultipliedColorU8([u8; 4]); -// Perfectly safe, since [u8; 4] is already Pod. -unsafe impl bytemuck::Zeroable for PremultipliedColorU8 {} -unsafe impl bytemuck::Pod for PremultipliedColorU8 {} - impl PremultipliedColorU8 { /// A transparent color. pub const TRANSPARENT: Self = PremultipliedColorU8::from_rgba_unchecked(0, 0, 0, 0); @@ -119,7 +225,7 @@ impl PremultipliedColorU8 { } /// Creates a new color. - pub(crate) const fn from_rgba_unchecked(r: u8, g: u8, b: u8, a: u8) -> Self { + pub const fn from_rgba_unchecked(r: u8, g: u8, b: u8, a: u8) -> Self { PremultipliedColorU8([r, g, b, a]) } @@ -156,6 +262,12 @@ impl PremultipliedColorU8 { self.alpha() == ALPHA_U8_OPAQUE } + /// Converts into a 16bpc `PremultipliedColorU16` via bit replication `(c << 8) | c`. + #[cfg(feature = "16bpc")] + pub fn to_color_u16(self) -> PremultipliedColorU16 { + PremultipliedColorU16::from_color_u8(self) + } + /// Returns a demultiplied color. pub fn demultiply(&self) -> ColorU8 { let alpha = self.alpha(); @@ -184,6 +296,121 @@ impl core::fmt::Debug for PremultipliedColorU8 { } } +/// A 64-bit premultiplied RGBA color value with 16 bits per channel. +#[cfg(feature = "16bpc")] +#[repr(transparent)] +#[derive(Copy, Clone, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct PremultipliedColorU16([u16; 4]); + +#[cfg(feature = "16bpc")] +impl PremultipliedColorU16 { + /// A transparent color. + pub const TRANSPARENT: Self = PremultipliedColorU16::from_rgba_unchecked(0, 0, 0, 0); + + /// Creates a new premultiplied color. + /// + /// RGB components must be <= alpha. + pub fn from_rgba(r: u16, g: u16, b: u16, a: u16) -> Option { + if r <= a && g <= a && b <= a { + Some(PremultipliedColorU16([r, g, b, a])) + } else { + None + } + } + + /// Creates a new color. + pub const fn from_rgba_unchecked(r: u16, g: u16, b: u16, a: u16) -> Self { + PremultipliedColorU16([r, g, b, a]) + } + + /// Returns color's red component. + /// + /// The value is <= alpha. + pub const fn red(self) -> u16 { + self.0[0] + } + + /// Returns color's green component. + /// + /// The value is <= alpha. + pub const fn green(self) -> u16 { + self.0[1] + } + + /// Returns color's blue component. + /// + /// The value is <= alpha. + pub const fn blue(self) -> u16 { + self.0[2] + } + + /// Returns color's alpha component. + pub const fn alpha(self) -> u16 { + self.0[3] + } + + /// Check that color is opaque. + /// + /// Alpha == 65535 + pub fn is_opaque(&self) -> bool { + self.alpha() == ALPHA_U16_OPAQUE + } + + /// Converts from 8-bit PremultipliedColorU8 via bit replication `(c << 8) | c`. + pub fn from_color_u8(c: PremultipliedColorU8) -> Self { + #[inline(always)] + fn up(v: u8) -> u16 { + let v = v as u16; + (v << 8) | v + } + PremultipliedColorU16([up(c.red()), up(c.green()), up(c.blue()), up(c.alpha())]) + } + + /// Converts to 8-bit PremultipliedColorU8 via `(c + 128) >> 8`. + pub fn to_color_u8(self) -> PremultipliedColorU8 { + #[inline(always)] + fn down(v: u16) -> u8 { + ((v as u32 + 128) >> 8).min(255) as u8 + } + PremultipliedColorU8::from_rgba_unchecked( + down(self.red()), + down(self.green()), + down(self.blue()), + down(self.alpha()), + ) + } + + /// Returns a demultiplied color. + pub fn demultiply(&self) -> ColorU16 { + let alpha = self.alpha(); + if alpha == ALPHA_U16_OPAQUE { + ColorU16(self.0) + } else if alpha == 0 { + ColorU16([0, 0, 0, 0]) + } else { + let a = alpha as f64 / 65535.0; + ColorU16::from_rgba( + ((self.red() as f64 / a + 0.5) as u64).min(65535) as u16, + ((self.green() as f64 / a + 0.5) as u64).min(65535) as u16, + ((self.blue() as f64 / a + 0.5) as u64).min(65535) as u16, + alpha, + ) + } + } +} + +#[cfg(feature = "16bpc")] +impl core::fmt::Debug for PremultipliedColorU16 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PremultipliedColorU16") + .field("r", &self.red()) + .field("g", &self.green()) + .field("b", &self.blue()) + .field("a", &self.alpha()) + .finish() + } +} + /// An RGBA color value, holding four floating point components. /// /// # Guarantees @@ -353,11 +580,31 @@ impl Color { } } + /// Creates a new color from 4 16-bit components. + /// + /// u16 will be divided by 65535 to get the float component. + #[cfg(feature = "16bpc")] + pub fn from_rgba16(r: u16, g: u16, b: u16, a: u16) -> Self { + Color { + r: NormalizedF32::new_clamped(r as f32 * (1.0 / 65535.0)), + g: NormalizedF32::new_clamped(g as f32 * (1.0 / 65535.0)), + b: NormalizedF32::new_clamped(b as f32 * (1.0 / 65535.0)), + a: NormalizedF32::new_clamped(a as f32 * (1.0 / 65535.0)), + } + } + /// Converts into `ColorU8`. pub fn to_color_u8(&self) -> ColorU8 { let c = color_f32_to_u8(self.r, self.g, self.b, self.a); ColorU8::from_rgba(c[0], c[1], c[2], c[3]) } + + /// Converts into `ColorU16`. + #[cfg(feature = "16bpc")] + pub fn to_color_u16(&self) -> ColorU16 { + let c = color_f32_to_u16(self.r, self.g, self.b, self.a); + ColorU16::from_rgba(c[0], c[1], c[2], c[3]) + } } /// A premultiplied RGBA color value, holding four floating point components. @@ -375,6 +622,19 @@ pub struct PremultipliedColor { } impl PremultipliedColor { + /// Creates a premultiplied color from components. + pub fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Option { + let r = NormalizedF32::new(r)?; + let g = NormalizedF32::new(g)?; + let b = NormalizedF32::new(b)?; + let a = NormalizedF32::new(a)?; + if r.get() <= a.get() && g.get() <= a.get() && b.get() <= a.get() { + Some(PremultipliedColor { r, g, b, a }) + } else { + None + } + } + /// Returns color's red component. /// /// - The value is guarantee to be in a 0..=1 range. @@ -426,6 +686,13 @@ impl PremultipliedColor { let c = color_f32_to_u8(self.r, self.g, self.b, self.a); PremultipliedColorU8::from_rgba_unchecked(c[0], c[1], c[2], c[3]) } + + /// Converts into `PremultipliedColorU16`. + #[cfg(feature = "16bpc")] + pub fn to_color_u16(&self) -> PremultipliedColorU16 { + let c = color_f32_to_u16(self.r, self.g, self.b, self.a); + PremultipliedColorU16::from_rgba_unchecked(c[0], c[1], c[2], c[3]) + } } /// Return a*b/255, rounding any fractional bits. @@ -434,6 +701,12 @@ pub fn premultiply_u8(c: u8, a: u8) -> u8 { ((prod + (prod >> 8)) >> 8) as u8 } +/// Return a*b/65535, rounding any fractional bits. +#[cfg(feature = "16bpc")] +pub fn premultiply_u16(c: u16, a: u16) -> u16 { + crate::math::mul_div65535_round(c as u32, a as u32) as u16 +} + fn color_f32_to_u8( r: NormalizedF32, g: NormalizedF32, @@ -448,6 +721,21 @@ fn color_f32_to_u8( ] } +#[cfg(feature = "16bpc")] +fn color_f32_to_u16( + r: NormalizedF32, + g: NormalizedF32, + b: NormalizedF32, + a: NormalizedF32, +) -> [u16; 4] { + [ + (r.get() * 65535.0 + 0.5) as u16, + (g.get() * 65535.0 + 0.5) as u16, + (b.get() * 65535.0 + 0.5) as u16, + (a.get() * 65535.0 + 0.5) as u16, + ] +} + /// The colorspace used to interpret pixel values. /// /// This is a very limited subset of SkColorSpace. @@ -598,4 +886,41 @@ mod tests { let bytes: &[u8] = bytemuck::cast_slice(slice); assert_eq!(bytes, &[0, 1, 2, 3, 10, 11, 12, 13]); } + + #[test] + #[cfg(feature = "16bpc")] + fn premultiply_u16_test() { + let c = ColorU16::from_rgba(1000, 2000, 3000, 4000).premultiply(); + // 1000 * 4000 / 65535 = 61.036 -> 61 + // 2000 * 4000 / 65535 = 122.07 -> 122 + // 3000 * 4000 / 65535 = 183.109 -> 183 + assert_eq!( + c, + PremultipliedColorU16::from_rgba_unchecked(61, 122, 183, 4000) + ); + } + + #[test] + #[cfg(feature = "16bpc")] + fn bytemuck_casts_rgba16() { + let slice = &[ + PremultipliedColorU16::from_rgba_unchecked(1, 2, 3, 4), + PremultipliedColorU16::from_rgba_unchecked(100, 200, 300, 400), + ]; + let bytes: &[u8] = bytemuck::cast_slice(slice); + let back: &[PremultipliedColorU16] = bytemuck::cast_slice(bytes); + assert_eq!(slice, back); + } + + #[test] + #[cfg(feature = "16bpc")] + fn bit_exact_mul_div65535_exhaustive_edge_cases() { + use crate::math::mul_div65535_round; + assert_eq!(mul_div65535_round(0, 0), 0); + assert_eq!(mul_div65535_round(65535, 65535), 65535); + assert_eq!(mul_div65535_round(0, 65535), 0); + assert_eq!(mul_div65535_round(65535, 0), 0); + assert_eq!(mul_div65535_round(32767, 65535), 32767); + assert_eq!(mul_div65535_round(32768, 65535), 32768); + } } diff --git a/src/geom.rs b/src/geom.rs index 98de38f..94310a5 100644 --- a/src/geom.rs +++ b/src/geom.rs @@ -148,6 +148,35 @@ impl ScreenIntRect { } } +pub trait IntSizeExt { + /// Converts the current size into a `IntRect` at a provided position. + fn to_screen_int_rect(&self, x: u32, y: u32) -> ScreenIntRect; +} + +impl IntSizeExt for IntSize { + fn to_screen_int_rect(&self, x: u32, y: u32) -> ScreenIntRect { + ScreenIntRect::from_xywh(x, y, self.width(), self.height()).unwrap() + } +} + +pub trait IntRectExt { + /// Converts into `ScreenIntRect`. + /// + /// # Checks + /// + /// - x >= 0 + /// - y >= 0 + fn to_screen_int_rect(&self) -> Option; +} + +impl IntRectExt for IntRect { + fn to_screen_int_rect(&self) -> Option { + let x = u32::try_from(self.x()).ok()?; + let y = u32::try_from(self.y()).ok()?; + ScreenIntRect::from_xywh(x, y, self.width(), self.height()) + } +} + #[cfg(test)] mod screen_int_rect_tests { use super::*; @@ -179,32 +208,3 @@ mod screen_int_rect_tests { assert_eq!(r.bottom(), 6); } } - -pub trait IntSizeExt { - /// Converts the current size into a `IntRect` at a provided position. - fn to_screen_int_rect(&self, x: u32, y: u32) -> ScreenIntRect; -} - -impl IntSizeExt for IntSize { - fn to_screen_int_rect(&self, x: u32, y: u32) -> ScreenIntRect { - ScreenIntRect::from_xywh(x, y, self.width(), self.height()).unwrap() - } -} - -pub trait IntRectExt { - /// Converts into `ScreenIntRect`. - /// - /// # Checks - /// - /// - x >= 0 - /// - y >= 0 - fn to_screen_int_rect(&self) -> Option; -} - -impl IntRectExt for IntRect { - fn to_screen_int_rect(&self) -> Option { - let x = u32::try_from(self.x()).ok()?; - let y = u32::try_from(self.y()).ok()?; - ScreenIntRect::from_xywh(x, y, self.width(), self.height()) - } -} diff --git a/src/lib.rs b/src/lib.rs index 6660881..52e356c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,10 +58,22 @@ mod painter; // Keep it under `pixmap` for a better order in the docs. pub use blend_mode::BlendMode; pub use color::{Color, ColorSpace, ColorU8, PremultipliedColor, PremultipliedColorU8}; +#[cfg(feature = "16bpc")] +pub use color::{ColorU16, PremultipliedColorU16}; pub use color::{ALPHA_OPAQUE, ALPHA_TRANSPARENT, ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT}; +#[cfg(feature = "16bpc")] +pub use color::{ALPHA_U16_OPAQUE, ALPHA_U16_TRANSPARENT}; pub use mask::{Mask, MaskType}; pub use painter::{FillRule, Paint}; -pub use pixmap::{Pixmap, PixmapMut, PixmapRef, BYTES_PER_PIXEL}; +pub use pipeline::HighPixel; +#[cfg(feature = "16bpc")] +pub use pixmap::{ + DynamicPixmap, DynamicPixmapMut, PixmapU16, PixmapU16Mut, PixmapU16Ref, SubPixmapU16Mut, +}; +pub use pixmap::{ + DynamicPixmapRef, Pixel, Pixmap, PixmapGeneric, PixmapMut, PixmapMutGeneric, PixmapRef, + PixmapRefGeneric, SubPixmapMut, SubPixmapMutGeneric, BYTES_PER_PIXEL, +}; pub use shaders::{FilterQuality, GradientStop, PixmapPaint, SpreadMode}; pub use shaders::{LinearGradient, Pattern, RadialGradient, Shader, SweepGradient}; diff --git a/src/mask.rs b/src/mask.rs index 420a813..fd4e40a 100644 --- a/src/mask.rs +++ b/src/mask.rs @@ -16,7 +16,7 @@ use crate::painter::DrawTiler; use crate::pipeline::RasterPipelineBlitter; use crate::pixmap::SubPixmapMut; use crate::scan; -use crate::{FillRule, PixmapRef}; +use crate::{FillRule, Pixel, PixmapRefGeneric}; /// A mask type. #[derive(Clone, Copy, PartialEq, Debug)] @@ -39,8 +39,8 @@ pub enum MaskType { /// It's way slower, but easier to implement. #[derive(Clone, PartialEq)] pub struct Mask { - data: Vec, - size: IntSize, + pub(crate) data: Vec, + pub(crate) size: IntSize, } impl Mask { @@ -54,7 +54,7 @@ impl Mask { } /// Creates a new mask from a `PixmapRef`. - pub fn from_pixmap(pixmap: PixmapRef, mask_type: MaskType) -> Self { + pub fn from_pixmap(pixmap: PixmapRefGeneric<'_, P>, mask_type: MaskType) -> Self { let data_len = pixmap.width() as usize * pixmap.height() as usize; let mut mask = Mask { data: vec![0; data_len], @@ -65,19 +65,20 @@ impl Mask { match mask_type { MaskType::Alpha => { for (p, a) in pixmap.pixels().iter().zip(mask.data.as_mut_slice()) { - *a = p.alpha(); + *a = p.to_u8().alpha(); } } MaskType::Luminance => { for (p, ma) in pixmap.pixels().iter().zip(mask.data.as_mut_slice()) { + let u8_p = p.to_u8(); // Normalize. - let mut r = f32::from(p.red()) / 255.0; - let mut g = f32::from(p.green()) / 255.0; - let mut b = f32::from(p.blue()) / 255.0; - let a = f32::from(p.alpha()) / 255.0; + let mut r = f32::from(u8_p.red()) / 255.0; + let mut g = f32::from(u8_p.green()) / 255.0; + let mut b = f32::from(u8_p.blue()) / 255.0; + let a = f32::from(u8_p.alpha()) / 255.0; // Demultiply. - if p.alpha() != 0 { + if u8_p.alpha() != 0 { r /= a; g /= a; b /= a; @@ -162,6 +163,7 @@ impl Mask { size: self.size, real_width: self.size.width() as usize, data: &mut self.data, + _marker: core::marker::PhantomData, } } @@ -174,6 +176,7 @@ impl Mask { size: rect.size(), real_width: self.size.width() as usize, data: &mut self.data[offset..], + _marker: core::marker::PhantomData, }) } diff --git a/src/math.rs b/src/math.rs index 861101d..c7367f8 100644 --- a/src/math.rs +++ b/src/math.rs @@ -24,6 +24,25 @@ pub fn bound(min: T, value: T, max: T) -> T { max.min(value).max(min) } +/// Computes `(a * b + 0x8000) / 65535` rounded to nearest integer for `a, b <= 65535`. +/// +/// Bit-exact with rounding division, computed entirely within 32-bit registers. +#[cfg(feature = "16bpc")] +#[inline(always)] +pub fn mul_div65535_round(a: u32, b: u32) -> u32 { + let prod = a * b + 0x8000; + (prod + (prod >> 16)) >> 16 +} + +/// Computes `(x + 0x8000) / 65535` rounded to nearest integer for `x <= 65535 * 65535`. +#[cfg(feature = "16bpc")] +#[inline(always)] +#[allow(dead_code)] +pub fn div65535_round(x: u32) -> u32 { + let temp = x + 0x8000; + (temp + (temp >> 16)) >> 16 +} + // Skia cites http://www.machinedlearnings.com/2011/06/fast-approximate-logarithm-exponential.html pub fn approx_powf(x: f32, y: f32) -> f32 { if x == 0.0 || x == 1.0 { diff --git a/src/painter.rs b/src/painter.rs index 624e00a..f30414b 100644 --- a/src/painter.rs +++ b/src/painter.rs @@ -10,8 +10,8 @@ use tiny_skia_path::{PathStroker, Scalar, SCALAR_MAX}; use crate::geom::ScreenIntRect; use crate::mask::SubMaskRef; -use crate::pipeline::{RasterPipelineBlitter, RasterPipelineBuilder}; -use crate::pixmap::SubPixmapMut; +use crate::pipeline::{HighPixel, RasterPipelineBlitter, RasterPipelineBuilder}; +use crate::pixmap::SubPixmapMutGeneric; use crate::scan; use crate::geom::IntSizeExt; @@ -105,7 +105,7 @@ impl Paint<'_> { } } -impl Pixmap { +impl PixmapGeneric

{ /// Draws a filled rectangle onto the pixmap. /// /// See [`PixmapMut::fill_rect`](struct.PixmapMut.html#method.fill_rect) for details. @@ -134,7 +134,7 @@ impl Pixmap { .fill_path(path, paint, fill_rule, transform, mask); } - /// Strokes a path. + /// Draws a stroked path onto the pixmap. /// /// See [`PixmapMut::stroke_path`](struct.PixmapMut.html#method.stroke_path) for details. pub fn stroke_path( @@ -149,14 +149,14 @@ impl Pixmap { .stroke_path(path, paint, stroke, transform, mask); } - /// Draws a `Pixmap` on top of the current `Pixmap`. + /// Draws a pixmap onto the pixmap. /// /// See [`PixmapMut::draw_pixmap`](struct.PixmapMut.html#method.draw_pixmap) for details. - pub fn draw_pixmap( + pub fn draw_pixmap<'b>( &mut self, x: i32, y: i32, - pixmap: PixmapRef, + pixmap: PixmapRefGeneric<'b, P>, paint: &PixmapPaint, transform: Transform, mask: Option<&Mask>, @@ -165,7 +165,7 @@ impl Pixmap { .draw_pixmap(x, y, pixmap, paint, transform, mask); } - /// Applies a masks. + /// Applies a mask. /// /// See [`PixmapMut::apply_mask`](struct.PixmapMut.html#method.apply_mask) for details. pub fn apply_mask(&mut self, mask: &Mask) { @@ -173,7 +173,7 @@ impl Pixmap { } } -impl PixmapMut<'_> { +impl<'a, P: HighPixel> PixmapMutGeneric<'a, P> { // TODO: accept NonZeroRect? /// Draws a filled rectangle onto the pixmap. /// @@ -452,7 +452,7 @@ impl PixmapMut<'_> { paint: &Paint, line_cap: LineCap, mask: Option, - pixmap: &mut SubPixmapMut, + pixmap: &mut SubPixmapMutGeneric<'_, P>, ) { let clip = pixmap.size.to_screen_int_rect(0, 0); let mut blitter = match RasterPipelineBlitter::new(paint, mask, pixmap) { @@ -469,35 +469,73 @@ impl PixmapMut<'_> { /// Draws a `Pixmap` on top of the current `Pixmap`. /// /// The same as filling a rectangle with a `pixmap` pattern. - pub fn draw_pixmap( + pub fn draw_pixmap<'b>( &mut self, x: i32, y: i32, - pixmap: PixmapRef, + pixmap: PixmapRefGeneric<'b, P>, paint: &PixmapPaint, transform: Transform, mask: Option<&Mask>, ) { let rect = pixmap.size().to_int_rect(x, y).to_rect(); - - // TODO: SkSpriteBlitter - // TODO: partially clipped - // TODO: clipped out - - // Translate pattern as well as bounds. let patt_transform = Transform::from_translate(x as f32, y as f32); + #[cfg(feature = "16bpc")] + if P::BYTES_PER_PIXEL != 4 { + if transform.is_identity() && mask.is_none() { + let dst_w = self.width() as i32; + let dst_h = self.height() as i32; + let src_w = pixmap.width() as i32; + let src_h = pixmap.height() as i32; + + let x0 = x.max(0); + let y0 = y.max(0); + let x1 = (x + src_w).min(dst_w); + let y1 = (y + src_h).min(dst_h); + + if x0 < x1 && y0 < y1 { + let opacity = paint.opacity.clamp(0.0, 1.0); + let op16 = (opacity * 65535.0 + 0.5) as u32; + + let src_pixels: &[PremultipliedColorU16] = bytemuck::cast_slice(pixmap.data()); + let dst_pixels: &mut [PremultipliedColorU16] = + bytemuck::cast_slice_mut(self.data_mut()); + + let mode = paint.blend_mode; + for cy in y0..y1 { + let src_row = (cy - y) * src_w; + let dst_row = cy * dst_w; + for cx in x0..x1 { + let mut sp = src_pixels[(src_row + (cx - x)) as usize]; + if op16 != 65535 { + let r = ((sp.red() as u32 * op16 + 32768) >> 16) as u16; + let g = ((sp.green() as u32 * op16 + 32768) >> 16) as u16; + let b = ((sp.blue() as u32 * op16 + 32768) >> 16) as u16; + let a = ((sp.alpha() as u32 * op16 + 32768) >> 16) as u16; + sp = PremultipliedColorU16::from_rgba_unchecked(r, g, b, a); + } + + let dp = &mut dst_pixels[(dst_row + cx) as usize]; + *dp = blend_u16_op(*dp, sp, mode); + } + } + } + return; + } + } + let paint = Paint { - shader: Pattern::new( + shader: Pattern::from_pixmap( pixmap, - SpreadMode::Pad, // Pad, otherwise we will get weird borders overlap. + SpreadMode::Pad, paint.quality, paint.opacity, patt_transform, ), blend_mode: paint.blend_mode, - anti_alias: false, // Skia doesn't use it too. - force_hq_pipeline: false, // Pattern will use hq anyway. + anti_alias: false, + force_hq_pipeline: false, colorspace: ColorSpace::default(), }; @@ -522,7 +560,7 @@ impl PixmapMut<'_> { } // Just a dummy. - let pixmap_src = PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(); + let pixmap_src = DynamicPixmapRef::dummy(); let mut p = RasterPipelineBuilder::new(); p.push(pipeline::Stage::LoadMaskU8); @@ -677,6 +715,471 @@ impl Iterator for DrawTiler { } } +#[cfg(feature = "16bpc")] +impl DynamicPixmap { + /// Draws a filled rectangle onto the dynamic pixmap. + pub fn fill_rect( + &mut self, + rect: Rect, + paint: &Paint, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmap::U8(p) => p.fill_rect(rect, paint, transform, mask), + DynamicPixmap::U16(p) => p.fill_rect(rect, paint, transform, mask), + } + } + + /// Draws a filled path onto the dynamic pixmap. + pub fn fill_path( + &mut self, + path: &Path, + paint: &Paint, + fill_rule: FillRule, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmap::U8(p) => p.fill_path(path, paint, fill_rule, transform, mask), + DynamicPixmap::U16(p) => p.fill_path(path, paint, fill_rule, transform, mask), + } + } + + /// Strokes a path onto the dynamic pixmap. + pub fn stroke_path( + &mut self, + path: &Path, + paint: &Paint, + stroke: &Stroke, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmap::U8(p) => p.stroke_path(path, paint, stroke, transform, mask), + DynamicPixmap::U16(p) => p.stroke_path(path, paint, stroke, transform, mask), + } + } + + /// Draws a `Pixmap` on top of the dynamic pixmap. + pub fn draw_pixmap( + &mut self, + x: i32, + y: i32, + pixmap: PixmapRef, + paint: &PixmapPaint, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmap::U8(p) => p.draw_pixmap(x, y, pixmap, paint, transform, mask), + DynamicPixmap::U16(p) => { + let u8_pixels = pixmap.pixels(); + let mut u16_pixmap = PixmapU16::new(pixmap.width(), pixmap.height()).unwrap(); + for (src, dst) in u8_pixels.iter().zip(u16_pixmap.pixels_mut().iter_mut()) { + *dst = PremultipliedColorU16::from_color_u8(*src); + } + p.draw_pixmap(x, y, u16_pixmap.as_ref(), paint, transform, mask); + } + } + } + + /// Applies a mask to the dynamic pixmap. + pub fn apply_mask(&mut self, mask: &Mask) { + match self { + DynamicPixmap::U8(p) => p.apply_mask(mask), + DynamicPixmap::U16(p) => p.apply_mask(mask), + } + } +} + +#[cfg(feature = "16bpc")] +impl DynamicPixmapMut<'_> { + /// Draws a filled rectangle onto the dynamic mutable pixmap. + pub fn fill_rect( + &mut self, + rect: Rect, + paint: &Paint, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmapMut::U8(p) => p.fill_rect(rect, paint, transform, mask), + DynamicPixmapMut::U16(p) => p.fill_rect(rect, paint, transform, mask), + } + } + + /// Draws a filled path onto the dynamic mutable pixmap. + pub fn fill_path( + &mut self, + path: &Path, + paint: &Paint, + fill_rule: FillRule, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmapMut::U8(p) => p.fill_path(path, paint, fill_rule, transform, mask), + DynamicPixmapMut::U16(p) => p.fill_path(path, paint, fill_rule, transform, mask), + } + } + + /// Strokes a path onto the dynamic mutable pixmap. + pub fn stroke_path( + &mut self, + path: &Path, + paint: &Paint, + stroke: &Stroke, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmapMut::U8(p) => p.stroke_path(path, paint, stroke, transform, mask), + DynamicPixmapMut::U16(p) => p.stroke_path(path, paint, stroke, transform, mask), + } + } + + /// Draws a `Pixmap` on top of the dynamic mutable pixmap. + pub fn draw_pixmap( + &mut self, + x: i32, + y: i32, + pixmap: PixmapRef, + paint: &PixmapPaint, + transform: Transform, + mask: Option<&Mask>, + ) { + match self { + DynamicPixmapMut::U8(p) => p.draw_pixmap(x, y, pixmap, paint, transform, mask), + DynamicPixmapMut::U16(p) => { + let u8_pixels = pixmap.pixels(); + let mut u16_pixmap = PixmapU16::new(pixmap.width(), pixmap.height()).unwrap(); + for (src, dst) in u8_pixels.iter().zip(u16_pixmap.pixels_mut().iter_mut()) { + *dst = PremultipliedColorU16::from_color_u8(*src); + } + p.draw_pixmap(x, y, u16_pixmap.as_ref(), paint, transform, mask); + } + } + } + + /// Applies a mask to the dynamic mutable pixmap. + pub fn apply_mask(&mut self, mask: &Mask) { + match self { + DynamicPixmapMut::U8(p) => p.apply_mask(mask), + DynamicPixmapMut::U16(p) => p.apply_mask(mask), + } + } +} + +#[cfg(feature = "16bpc")] +#[inline] +fn blend_u16_op( + dst: PremultipliedColorU16, + src: PremultipliedColorU16, + mode: BlendMode, +) -> PremultipliedColorU16 { + match mode { + BlendMode::Source => src, + BlendMode::Destination => dst, + BlendMode::Clear => PremultipliedColorU16::TRANSPARENT, + BlendMode::SourceOver => { + let inv_a = 65535 - src.alpha() as u32; + let r = src.red() as u32 + ((dst.red() as u32 * inv_a + 32768) >> 16); + let g = src.green() as u32 + ((dst.green() as u32 * inv_a + 32768) >> 16); + let b = src.blue() as u32 + ((dst.blue() as u32 * inv_a + 32768) >> 16); + let a = src.alpha() as u32 + ((dst.alpha() as u32 * inv_a + 32768) >> 16); + PremultipliedColorU16::from_rgba_unchecked( + r.min(65535) as u16, + g.min(65535) as u16, + b.min(65535) as u16, + a.min(65535) as u16, + ) + } + BlendMode::DestinationOver => { + let inv_a = 65535 - dst.alpha() as u32; + let r = dst.red() as u32 + ((src.red() as u32 * inv_a + 32768) >> 16); + let g = dst.green() as u32 + ((src.green() as u32 * inv_a + 32768) >> 16); + let b = dst.blue() as u32 + ((src.blue() as u32 * inv_a + 32768) >> 16); + let a = dst.alpha() as u32 + ((src.alpha() as u32 * inv_a + 32768) >> 16); + PremultipliedColorU16::from_rgba_unchecked( + r.min(65535) as u16, + g.min(65535) as u16, + b.min(65535) as u16, + a.min(65535) as u16, + ) + } + BlendMode::SourceIn => { + let a = dst.alpha() as u32; + let r = ((src.red() as u32 * a + 32768) >> 16) as u16; + let g = ((src.green() as u32 * a + 32768) >> 16) as u16; + let b = ((src.blue() as u32 * a + 32768) >> 16) as u16; + let a = ((src.alpha() as u32 * a + 32768) >> 16) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::DestinationIn => { + let a = src.alpha() as u32; + let r = ((dst.red() as u32 * a + 32768) >> 16) as u16; + let g = ((dst.green() as u32 * a + 32768) >> 16) as u16; + let b = ((dst.blue() as u32 * a + 32768) >> 16) as u16; + let a = ((dst.alpha() as u32 * a + 32768) >> 16) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::SourceOut => { + let inv_a = 65535 - dst.alpha() as u32; + let r = ((src.red() as u32 * inv_a + 32768) >> 16) as u16; + let g = ((src.green() as u32 * inv_a + 32768) >> 16) as u16; + let b = ((src.blue() as u32 * inv_a + 32768) >> 16) as u16; + let a = ((src.alpha() as u32 * inv_a + 32768) >> 16) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::DestinationOut => { + let inv_a = 65535 - src.alpha() as u32; + let r = ((dst.red() as u32 * inv_a + 32768) >> 16) as u16; + let g = ((dst.green() as u32 * inv_a + 32768) >> 16) as u16; + let b = ((dst.blue() as u32 * inv_a + 32768) >> 16) as u16; + let a = ((dst.alpha() as u32 * inv_a + 32768) >> 16) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::SourceAtop => { + let da = dst.alpha() as u32; + let inv_sa = 65535 - src.alpha() as u32; + let r = ((src.red() as u32 * da + dst.red() as u32 * inv_sa + 32768) >> 16) as u16; + let g = ((src.green() as u32 * da + dst.green() as u32 * inv_sa + 32768) >> 16) as u16; + let b = ((src.blue() as u32 * da + dst.blue() as u32 * inv_sa + 32768) >> 16) as u16; + let a = dst.alpha(); + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::DestinationAtop => { + let sa = src.alpha() as u32; + let inv_da = 65535 - dst.alpha() as u32; + let r = ((dst.red() as u32 * sa + src.red() as u32 * inv_da + 32768) >> 16) as u16; + let g = ((dst.green() as u32 * sa + src.green() as u32 * inv_da + 32768) >> 16) as u16; + let b = ((dst.blue() as u32 * sa + src.blue() as u32 * inv_da + 32768) >> 16) as u16; + let a = src.alpha(); + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::Xor => { + let inv_da = 65535 - dst.alpha() as u32; + let inv_sa = 65535 - src.alpha() as u32; + let r = ((src.red() as u32 * inv_da + dst.red() as u32 * inv_sa + 32768) >> 16) as u16; + let g = + ((src.green() as u32 * inv_da + dst.green() as u32 * inv_sa + 32768) >> 16) as u16; + let b = + ((src.blue() as u32 * inv_da + dst.blue() as u32 * inv_sa + 32768) >> 16) as u16; + let a = + ((src.alpha() as u32 * inv_da + dst.alpha() as u32 * inv_sa + 32768) >> 16) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + BlendMode::Plus => { + let r = (dst.red() as u32 + src.red() as u32).min(65535) as u16; + let g = (dst.green() as u32 + src.green() as u32).min(65535) as u16; + let b = (dst.blue() as u32 + src.blue() as u32).min(65535) as u16; + let a = (dst.alpha() as u32 + src.alpha() as u32).min(65535) as u16; + PremultipliedColorU16::from_rgba_unchecked(r, g, b, a) + } + _ => { + let sr = src.red() as f32 / 65535.0; + let sg = src.green() as f32 / 65535.0; + let sb = src.blue() as f32 / 65535.0; + let sa = src.alpha() as f32 / 65535.0; + + let dr = dst.red() as f32 / 65535.0; + let dg = dst.green() as f32 / 65535.0; + let db = dst.blue() as f32 / 65535.0; + let da = dst.alpha() as f32 / 65535.0; + + let (r, g, b) = match mode { + BlendMode::Hue => { + let (mut rr, mut gg, mut bb) = (sr * sa, sg * sa, sb * sa); + set_sat(&mut rr, &mut gg, &mut bb, sat(dr, dg, db) * sa); + set_lum(&mut rr, &mut gg, &mut bb, lum(dr, dg, db) * sa); + clip_color(&mut rr, &mut gg, &mut bb, sa * da); + ( + sr * (1.0 - da) + dr * (1.0 - sa) + rr, + sg * (1.0 - da) + dg * (1.0 - sa) + gg, + sb * (1.0 - da) + db * (1.0 - sa) + bb, + ) + } + BlendMode::Saturation => { + let (mut rr, mut gg, mut bb) = (dr * sa, dg * sa, db * sa); + set_sat(&mut rr, &mut gg, &mut bb, sat(sr, sg, sb) * da); + set_lum(&mut rr, &mut gg, &mut bb, lum(dr, dg, db) * sa); + clip_color(&mut rr, &mut gg, &mut bb, sa * da); + ( + sr * (1.0 - da) + dr * (1.0 - sa) + rr, + sg * (1.0 - da) + dg * (1.0 - sa) + gg, + sb * (1.0 - da) + db * (1.0 - sa) + bb, + ) + } + BlendMode::Color => { + let (mut rr, mut gg, mut bb) = (sr * da, sg * da, sb * da); + set_lum(&mut rr, &mut gg, &mut bb, lum(dr, dg, db) * sa); + clip_color(&mut rr, &mut gg, &mut bb, sa * da); + ( + sr * (1.0 - da) + dr * (1.0 - sa) + rr, + sg * (1.0 - da) + dg * (1.0 - sa) + gg, + sb * (1.0 - da) + db * (1.0 - sa) + bb, + ) + } + BlendMode::Luminosity => { + let (mut rr, mut gg, mut bb) = (dr * sa, dg * sa, db * sa); + set_lum(&mut rr, &mut gg, &mut bb, lum(sr, sg, sb) * da); + clip_color(&mut rr, &mut gg, &mut bb, sa * da); + ( + sr * (1.0 - da) + dr * (1.0 - sa) + rr, + sg * (1.0 - da) + dg * (1.0 - sa) + gg, + sb * (1.0 - da) + db * (1.0 - sa) + bb, + ) + } + _ => { + let blend_ch = |s: f32, d: f32| -> f32 { + match mode { + BlendMode::Multiply => s * (1.0 - da) + d * (1.0 - sa) + s * d, + BlendMode::Screen => s + d - s * d, + BlendMode::Darken => s + d - (s * da).max(d * sa), + BlendMode::Lighten => s + d - (s * da).min(d * sa), + BlendMode::Difference => s + d - 2.0 * (s * da).min(d * sa), + BlendMode::Exclusion => s + d - 2.0 * s * d, + BlendMode::ColorDodge => { + if d <= 0.0 { + s * (1.0 - da) + } else if s >= sa { + s + d * (1.0 - sa) + } else { + sa * da.min((d * sa) / (sa - s)) + + s * (1.0 - da) + + d * (1.0 - sa) + } + } + BlendMode::ColorBurn => { + if d >= da { + d + s * (1.0 - da) + } else if s <= 0.0 { + d * (1.0 - sa) + } else { + sa * (da - da.min((da - d) * sa / s)) + + s * (1.0 - da) + + d * (1.0 - sa) + } + } + BlendMode::Overlay => { + let b = if 2.0 * d <= da { + 2.0 * s * d + } else { + sa * da - 2.0 * (da - d) * (sa - s) + }; + s * (1.0 - da) + d * (1.0 - sa) + b + } + BlendMode::HardLight => { + let b = if 2.0 * s <= sa { + 2.0 * s * d + } else { + sa * da - 2.0 * (da - d) * (sa - s) + }; + s * (1.0 - da) + d * (1.0 - sa) + b + } + BlendMode::SoftLight => { + let m = if da > 0.0 { d / da } else { 0.0 }; + let s2 = 2.0 * s; + let m4 = 4.0 * m; + let dark_src = d * (sa + (s2 - sa) * (1.0 - m)); + let dark_dst = (m4 * m4 + m4) * (m - 1.0) + 7.0 * m; + let lite_dst = m.sqrt() - m; + let lite_src = d * sa + + da * (s2 - sa) + * if 4.0 * d <= da { dark_dst } else { lite_dst }; + s * (1.0 - da) + + d * (1.0 - sa) + + if s2 <= sa { dark_src } else { lite_src } + } + _ => s + d * (1.0 - sa), + } + }; + (blend_ch(sr, dr), blend_ch(sg, dg), blend_ch(sb, db)) + } + }; + + let a = sa + da - sa * da; + + let r16 = (r.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16; + let g16 = (g.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16; + let b16 = (b.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16; + let a16 = (a.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16; + PremultipliedColorU16::from_rgba_unchecked(r16, g16, b16, a16) + } + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn sat(r: f32, g: f32, b: f32) -> f32 { + r.max(g.max(b)) - r.min(g.min(b)) +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn lum(r: f32, g: f32, b: f32) -> f32 { + r * 0.30 + g * 0.59 + b * 0.11 +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn set_sat(r: &mut f32, g: &mut f32, b: &mut f32, s: f32) { + let mn = r.min(g.min(*b)); + let mx = r.max(g.max(*b)); + let sat = mx - mn; + if sat > 0.0 { + *r = (*r - mn) * s / sat; + *g = (*g - mn) * s / sat; + *b = (*b - mn) * s / sat; + } else { + *r = 0.0; + *g = 0.0; + *b = 0.0; + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn set_lum(r: &mut f32, g: &mut f32, b: &mut f32, l: f32) { + let diff = l - lum(*r, *g, *b); + *r += diff; + *g += diff; + *b += diff; +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn clip_color(r: &mut f32, g: &mut f32, b: &mut f32, a: f32) { + let mn = r.min(g.min(*b)); + let mx = r.max(g.max(*b)); + let l = lum(*r, *g, *b); + + if mn < 0.0 { + if (l - mn).abs() > 0.0 { + *r = l + (*r - l) * l / (l - mn); + *g = l + (*g - l) * l / (l - mn); + *b = l + (*b - l) * l / (l - mn); + } else { + *r = l; + *g = l; + *b = l; + } + } + + if mx > a { + if (mx - l).abs() > 0.0 { + *r = l + (*r - l) * (a - l) / (mx - l); + *g = l + (*g - l) * (a - l) / (mx - l); + *b = l + (*b - l) * (a - l) / (mx - l); + } else { + *r = l; + *g = l; + *b = l; + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/pipeline/blitter.rs b/src/pipeline/blitter.rs index 794c3d0..2498833 100644 --- a/src/pipeline/blitter.rs +++ b/src/pipeline/blitter.rs @@ -4,7 +4,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -use crate::{BlendMode, Color, LengthU32, Paint, PixmapRef, PremultipliedColorU8, Shader}; +use crate::{ + BlendMode, Color, DynamicPixmapRef, LengthU32, Paint, Pixel, PremultipliedColorU8, Shader, +}; use crate::{ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT}; use crate::alpha_runs::AlphaRun; @@ -13,25 +15,26 @@ use crate::color::AlphaU8; use crate::geom::ScreenIntRect; use crate::mask::SubMaskRef; use crate::math::LENGTH_U32_ONE; -use crate::pipeline::{self, RasterPipeline, RasterPipelineBuilder}; -use crate::pixmap::SubPixmapMut; +use crate::pipeline::highp::HighPixel; +use crate::pipeline::{self, RasterPipelineBuilder, RasterPipelineGeneric}; +use crate::pixmap::SubPixmapMutGeneric; -pub struct RasterPipelineBlitter<'a, 'b: 'a> { +pub struct RasterPipelineBlitter<'a, 'b: 'a, P: HighPixel> { mask: Option>, - pixmap_src: PixmapRef<'a>, - pixmap: &'a mut SubPixmapMut<'b>, - memset2d_color: Option, - blit_anti_h_rp: RasterPipeline, - blit_rect_rp: RasterPipeline, - blit_mask_rp: RasterPipeline, + pixmap_src: DynamicPixmapRef<'a>, + pixmap: &'a mut SubPixmapMutGeneric<'b, P>, + memset2d_color: Option

, + blit_anti_h_rp: RasterPipelineGeneric

, + blit_rect_rp: RasterPipelineGeneric

, + blit_mask_rp: RasterPipelineGeneric

, is_mask: bool, } -impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { +impl<'a, 'b: 'a, P: HighPixel> RasterPipelineBlitter<'a, 'b, P> { pub fn new( paint: &Paint<'a>, mask: Option>, - pixmap: &'a mut SubPixmapMut<'b>, + pixmap: &'a mut SubPixmapMutGeneric<'b, P>, ) -> Option { // Make sure that `mask` has the same size as `pixmap`. if let Some(mask) = mask { @@ -66,14 +69,14 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { // Unlike Skia, our shader cannot be constant. // Therefore there is no need to run a raster pipeline to get shader's color. if let Shader::SolidColor(ref color) = paint.shader { - memset2d_color = Some(color.premultiply().to_color_u8()); + memset2d_color = Some(P::from_color(*color)); } }; // Clear is just a transparent color memset. if blend_mode == BlendMode::Clear && !paint.anti_alias && mask.is_none() { blend_mode = BlendMode::Source; - memset2d_color = Some(PremultipliedColorU8::TRANSPARENT); + memset2d_color = Some(P::from_color(Color::TRANSPARENT)); } let blit_anti_h_rp = { @@ -196,7 +199,7 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { let pixmap_src = match paint.shader { Shader::Pattern(ref patt) => patt.pixmap, // Just a dummy one. - _ => PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(), + _ => DynamicPixmapRef::dummy(), }; Some(RasterPipelineBlitter { @@ -210,11 +213,13 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { is_mask: false, }) } +} - pub fn new_mask(pixmap: &'a mut SubPixmapMut<'b>) -> Option { +impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b, PremultipliedColorU8> { + pub fn new_mask(pixmap: &'a mut SubPixmapMutGeneric<'b, PremultipliedColorU8>) -> Option { let color = Color::WHITE.premultiply(); - let memset2d_color = Some(color.to_color_u8()); + let memset2d_color = Some(PremultipliedColorU8::from_color(Color::WHITE)); let blit_anti_h_rp = { let mut p = RasterPipelineBuilder::new(); @@ -243,7 +248,7 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { Some(RasterPipelineBlitter { mask: None, - pixmap_src: PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(), + pixmap_src: DynamicPixmapRef::dummy(), pixmap, memset2d_color, blit_anti_h_rp, @@ -254,7 +259,7 @@ impl<'a, 'b: 'a> RasterPipelineBlitter<'a, 'b> { } } -impl Blitter for RasterPipelineBlitter<'_, '_> { +impl Blitter for RasterPipelineBlitter<'_, '_, P> { fn blit_h(&mut self, x: u32, y: u32, width: LengthU32) { let r = ScreenIntRect::from_xywh_safe(x, y, width, LENGTH_U32_ONE); self.blit_rect(&r); @@ -333,25 +338,22 @@ impl Blitter for RasterPipelineBlitter<'_, '_> { fn blit_rect(&mut self, rect: &ScreenIntRect) { if let Some(c) = self.memset2d_color { + let real_width = self.pixmap.real_width; if self.is_mask { + let mask_data: &mut [u8] = bytemuck::cast_slice_mut(self.pixmap.data); for y in 0..rect.height() { - let start = self - .pixmap - .offset(rect.x() as usize, (rect.y() + y) as usize); + let start = real_width * (rect.y() + y) as usize + rect.x() as usize; let end = start + rect.width() as usize; - self.pixmap.data[start..end] - .iter_mut() - .for_each(|p| *p = c.alpha()); + mask_data[start..end].fill(255); } } else { for y in 0..rect.height() { - let start = self - .pixmap - .offset(rect.x() as usize, (rect.y() + y) as usize); - let end = start + rect.width() as usize; - self.pixmap.pixels_mut()[start..end] - .iter_mut() - .for_each(|p| *p = c); + let start = (real_width * (rect.y() + y) as usize + rect.x() as usize) + * P::BYTES_PER_PIXEL; + let end = start + rect.width() as usize * P::BYTES_PER_PIXEL; + let slice: &mut [P] = + bytemuck::cast_slice_mut(&mut self.pixmap.data[start..end]); + slice.fill(c); } } diff --git a/src/pipeline/highp.rs b/src/pipeline/highp.rs index 214e31a..ec6665b 100644 --- a/src/pipeline/highp.rs +++ b/src/pipeline/highp.rs @@ -10,27 +10,89 @@ A high precision raster pipeline implementation. Unlike lowp, this one implements all stages. Just like Skia, this pipeline is implemented using f32x8. - -For some reason, we are almost 2x slower. Maybe because Skia uses clang's vector extensions -and we're using a manual implementation. */ -use crate::{PremultipliedColorU8, SpreadMode, PixmapRef}; +use crate::{Pixel, PremultipliedColorU8, SpreadMode, PixmapRef}; +#[cfg(feature = "16bpc")] +use crate::{DynamicPixmapRef, PremultipliedColorU16, SubPixmapU16Mut}; use crate::geom::ScreenIntRect; -use crate::pixmap::SubPixmapMut; +use crate::pixmap::{SubPixmapMut, SubPixmapMutGeneric}; use crate::wide::{f32x8, i32x8, u32x8}; pub const STAGE_WIDTH: usize = 8; +/// Internal high-precision raster pipeline pixel trait. +pub trait HighPixel: Pixel { + /// Executes low-precision pipeline for 8bpc if compatible. + fn run_lowp( + functions: &[super::lowp::StageFn], + tail_functions: &[super::lowp::StageFn], + rect: &ScreenIntRect, + aa_mask_ctx: super::AAMaskCtx, + mask_ctx: super::MaskCtx, + ctx: &mut super::Context, + pixmap_dst: &mut SubPixmapMutGeneric<'_, Self>, + ); +} + +impl HighPixel for PremultipliedColorU8 { + fn run_lowp( + functions: &[super::lowp::StageFn], + tail_functions: &[super::lowp::StageFn], + rect: &ScreenIntRect, + aa_mask_ctx: super::AAMaskCtx, + mask_ctx: super::MaskCtx, + ctx: &mut super::Context, + pixmap_dst: &mut SubPixmapMutGeneric<'_, Self>, + ) { + super::lowp::start( + functions, + tail_functions, + rect, + aa_mask_ctx, + mask_ctx, + ctx, + pixmap_dst, + ); + } +} + +#[cfg(feature = "16bpc")] +impl HighPixel for PremultipliedColorU16 { + fn run_lowp( + _functions: &[super::lowp::StageFn], + _tail_functions: &[super::lowp::StageFn], + _rect: &ScreenIntRect, + _aa_mask_ctx: super::AAMaskCtx, + _mask_ctx: super::MaskCtx, + _ctx: &mut super::Context, + _pixmap_dst: &mut SubPixmapMutGeneric<'_, Self>, + ) { + unreachable!("lowp is not supported for 16bpc"); + } +} + pub type StageFn = fn(p: &mut Pipeline); +#[cfg(feature = "16bpc")] +pub type StageFnU16 = fn(p: &mut PipelineU16); + +pub fn fn_ptr(f: StageFn) -> usize { + f as usize +} + +#[cfg(feature = "16bpc")] +pub fn fn_ptr_u16(f: StageFnU16) -> usize { + f as usize +} + pub struct Pipeline<'a, 'b: 'a> { index: usize, functions: &'a [StageFn], pixmap_src: PixmapRef<'a>, pixmap_dst: &'a mut SubPixmapMut<'b>, - ctx: &'a mut super::Context, // TODO: remove mut + ctx: &'a mut super::Context, mask_ctx: super::MaskCtx<'a>, aa_mask_ctx: super::AAMaskCtx, r: f32x8, @@ -55,6 +117,38 @@ impl Pipeline<'_, '_> { } } +#[cfg(feature = "16bpc")] +pub struct PipelineU16<'a, 'b: 'a> { + index: usize, + functions: &'a [StageFnU16], + pixmap_src: DynamicPixmapRef<'a>, + pixmap_dst: &'a mut SubPixmapU16Mut<'b>, + ctx: &'a mut super::Context, + mask_ctx: super::MaskCtx<'a>, + aa_mask_ctx: super::AAMaskCtx, + r: f32x8, + g: f32x8, + b: f32x8, + a: f32x8, + dr: f32x8, + dg: f32x8, + db: f32x8, + da: f32x8, + tail: usize, + dx: usize, + dy: usize, +} + +#[cfg(feature = "16bpc")] +impl PipelineU16<'_, '_> { + #[inline(always)] + fn next_stage(&mut self) { + let next: fn(&mut Self) = self.functions[self.index]; + self.index += 1; + next(self); + } +} + // Must be in the same order as raster_pipeline::Stage pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[ move_source_to_destination, @@ -138,11 +232,89 @@ pub const STAGES: &[StageFn; super::STAGES_COUNT] = &[ gamma_compress_srgb, ]; -pub fn fn_ptr(f: StageFn) -> *const () { - f as *const () -} +#[cfg(feature = "16bpc")] +pub const STAGES_U16: &[StageFnU16; super::STAGES_COUNT] = &[ + move_source_to_destination_u16, + move_destination_to_source_u16, + clamp_0_u16, + clamp_a_u16, + premultiply_u16, + uniform_color_u16, + seed_shader_u16, + load_dst_u16, + store_u16, + load_dst_u8_u16, + store_u8_u16, + gather_u16, + load_mask_u8_u16, + mask_u8_u16, + scale_u8_u16, + lerp_u8_u16, + scale_1_float_u16, + lerp_1_float_u16, + destination_atop_u16, + destination_in_u16, + destination_out_u16, + destination_over_u16, + source_atop_u16, + source_in_u16, + source_out_u16, + source_over_u16, + clear_u16, + modulate_u16, + multiply_u16, + plus_u16, + screen_u16, + xor_u16, + color_burn_u16, + color_dodge_u16, + darken_u16, + difference_u16, + exclusion_u16, + hard_light_u16, + lighten_u16, + overlay_u16, + soft_light_u16, + hue_u16, + saturation_u16, + color_u16, + luminosity_u16, + source_over_rgba_u16, + transform_u16, + reflect_u16, + repeat_u16, + bilinear_u16, + bicubic_u16, + pad_x1_u16, + reflect_x1_u16, + repeat_x1_u16, + gradient_u16, + evenly_spaced_2_stop_gradient_u16, + xy_to_unit_angle_u16, + xy_to_radius_u16, + xy_to_2pt_conical_focal_on_circle_u16, + xy_to_2pt_conical_well_behaved_u16, + xy_to_2pt_conical_smaller_u16, + xy_to_2pt_conical_greater_u16, + xy_to_2pt_conical_strip_u16, + mask_2pt_conical_nan_u16, + mask_2pt_conical_degenerates_u16, + apply_vector_mask_u16, + alter_2pt_conical_compensate_focal_u16, + alter_2pt_conical_unswap_u16, + negate_x_u16, + apply_concentric_scale_bias_u16, + gamma_expand_2_u16, + gamma_expand_dst_2_u16, + gamma_compress_2_u16, + gamma_expand_22_u16, + gamma_expand_dst_22_u16, + gamma_compress_22_u16, + gamma_expand_srgb_u16, + gamma_expand_dst_srgb_u16, + gamma_compress_srgb_u16, +]; -#[inline(never)] pub fn start( functions: &[StageFn], functions_tail: &[StageFn], @@ -199,51 +371,308 @@ pub fn start( } } -fn move_source_to_destination(p: &mut Pipeline) { +#[cfg(feature = "16bpc")] +pub fn start_u16( + functions: &[StageFnU16], + functions_tail: &[StageFnU16], + rect: &ScreenIntRect, + aa_mask_ctx: super::AAMaskCtx, + mask_ctx: super::MaskCtx, + ctx: &mut super::Context, + pixmap_src: DynamicPixmapRef, + pixmap_dst: &mut SubPixmapU16Mut, +) { + let mut p = PipelineU16 { + index: 0, + functions: &[], + pixmap_src, + pixmap_dst, + mask_ctx, + aa_mask_ctx, + ctx, + r: f32x8::default(), + g: f32x8::default(), + b: f32x8::default(), + a: f32x8::default(), + dr: f32x8::default(), + dg: f32x8::default(), + db: f32x8::default(), + da: f32x8::default(), + tail: 0, + dx: 0, + dy: 0, + }; + + for y in rect.y()..rect.bottom() { + let mut x = rect.x() as usize; + let end = rect.right() as usize; + + p.functions = functions; + while x + STAGE_WIDTH <= end { + p.index = 0; + p.dx = x; + p.dy = y as usize; + p.tail = STAGE_WIDTH; + p.next_stage(); + x += STAGE_WIDTH; + } + + if x != end { + p.index = 0; + p.functions = functions_tail; + p.dx = x; + p.dy = y as usize; + p.tail = end - x; + p.next_stage(); + } + } +} + +macro_rules! stage_fn { + (fn $name:ident, $name_u16:ident, |$p:ident| $body:block) => { + #[allow(unused_variables)] + fn $name($p: &mut Pipeline) $body + + #[cfg(feature = "16bpc")] + #[allow(unused_variables)] + fn $name_u16($p: &mut PipelineU16) $body + }; + (pub fn $name:ident, $name_u16:ident, |$p:ident| $body:block) => { + #[allow(unused_variables)] + pub fn $name($p: &mut Pipeline) $body + + #[cfg(feature = "16bpc")] + #[allow(unused_variables)] + pub fn $name_u16($p: &mut PipelineU16) $body + }; +} + +pub fn load_dst(p: &mut Pipeline) { + load_8888(p.pixmap_dst.slice4_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.next_stage(); +} + +pub fn load_dst_tail(p: &mut Pipeline) { + load_8888_tail(p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.next_stage(); +} + +pub fn store(p: &mut Pipeline) { + store_8888(&p.r, &p.g, &p.b, &p.a, p.pixmap_dst.slice4_at_xy(p.dx, p.dy)); + p.next_stage(); +} + +pub fn store_tail(p: &mut Pipeline) { + store_8888_tail(&p.r, &p.g, &p.b, &p.a, p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy)); + p.next_stage(); +} + +pub fn load_dst_u8(_: &mut Pipeline) {} +pub fn load_dst_u8_tail(_: &mut Pipeline) {} +pub fn store_u8(_: &mut Pipeline) {} +pub fn store_u8_tail(_: &mut Pipeline) {} + +pub fn gather(p: &mut Pipeline) { + let ix = gather_ix(p.pixmap_src, p.r, p.g); + load_8888(&p.pixmap_src.gather(ix), &mut p.r, &mut p.g, &mut p.b, &mut p.a); + p.next_stage(); +} + +fn bilinear(p: &mut Pipeline) { + let x = p.r; + let fx = (x + f32x8::splat(0.5)).fract(); + let y = p.g; + let fy = (y + f32x8::splat(0.5)).fract(); + let one = f32x8::splat(1.0); + let wx = [one - fx, fx]; + let wy = [one - fy, fy]; + + sampler_2x2(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); + + p.next_stage(); +} + +fn bicubic(p: &mut Pipeline) { + let x = p.r; + let fx = (x + f32x8::splat(0.5)).fract(); + let y = p.g; + let fy = (y + f32x8::splat(0.5)).fract(); + let one = f32x8::splat(1.0); + let wx = [bicubic_far(one - fx), bicubic_near(one - fx), bicubic_near(fx), bicubic_far(fx)]; + let wy = [bicubic_far(one - fy), bicubic_near(one - fy), bicubic_near(fy), bicubic_far(fy)]; + + sampler_4x4(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); + + p.next_stage(); +} + +pub fn source_over_rgba(p: &mut Pipeline) { + let pixels = p.pixmap_dst.slice4_at_xy(p.dx, p.dy); + load_8888(pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.r = mad(p.dr, inv(p.a), p.r); + p.g = mad(p.dg, inv(p.a), p.g); + p.b = mad(p.db, inv(p.a), p.b); + p.a = mad(p.da, inv(p.a), p.a); + store_8888(&p.r, &p.g, &p.b, &p.a, pixels); + p.next_stage(); +} + +pub fn source_over_rgba_tail(p: &mut Pipeline) { + let pixels = p.pixmap_dst.slice_at_xy(p.dx, p.dy); + load_8888_tail(p.tail, pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.r = mad(p.dr, inv(p.a), p.r); + p.g = mad(p.dg, inv(p.a), p.g); + p.b = mad(p.db, inv(p.a), p.b); + p.a = mad(p.da, inv(p.a), p.a); + store_8888_tail(&p.r, &p.g, &p.b, &p.a, p.tail, pixels); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn load_dst_u16(p: &mut PipelineU16) { + load_16161616(p.pixmap_dst.slice4_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn load_dst_tail_u16(p: &mut PipelineU16) { + load_16161616_tail(p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn store_u16(p: &mut PipelineU16) { + store_16161616(&p.r, &p.g, &p.b, &p.a, p.pixmap_dst.slice4_at_xy(p.dx, p.dy)); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn store_tail_u16(p: &mut PipelineU16) { + store_16161616_tail(&p.r, &p.g, &p.b, &p.a, p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy)); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn load_dst_u8_u16(_: &mut PipelineU16) {} +#[cfg(feature = "16bpc")] +pub fn load_dst_u8_tail_u16(_: &mut PipelineU16) {} +#[cfg(feature = "16bpc")] +pub fn store_u8_u16(_: &mut PipelineU16) {} +#[cfg(feature = "16bpc")] +pub fn store_u8_tail_u16(_: &mut PipelineU16) {} + +#[cfg(feature = "16bpc")] +pub fn gather_u16(p: &mut PipelineU16) { + let ix = gather_ix_dyn(p.pixmap_src, p.r, p.g); + match p.pixmap_src { + DynamicPixmapRef::U8(src) => { + load_8888(&src.gather(ix), &mut p.r, &mut p.g, &mut p.b, &mut p.a); + } + DynamicPixmapRef::U16(src) => { + load_16161616(&src.gather(ix), &mut p.r, &mut p.g, &mut p.b, &mut p.a); + } + } + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +fn bilinear_u16(p: &mut PipelineU16) { + let x = p.r; + let fx = (x + f32x8::splat(0.5)).fract(); + let y = p.g; + let fy = (y + f32x8::splat(0.5)).fract(); + let one = f32x8::splat(1.0); + let wx = [one - fx, fx]; + let wy = [one - fy, fy]; + + sampler_2x2_dyn(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); + + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +fn bicubic_u16(p: &mut PipelineU16) { + let x = p.r; + let fx = (x + f32x8::splat(0.5)).fract(); + let y = p.g; + let fy = (y + f32x8::splat(0.5)).fract(); + let one = f32x8::splat(1.0); + let wx = [bicubic_far(one - fx), bicubic_near(one - fx), bicubic_near(fx), bicubic_far(fx)]; + let wy = [bicubic_far(one - fy), bicubic_near(one - fy), bicubic_near(fy), bicubic_far(fy)]; + + sampler_4x4_dyn(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); + + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn source_over_rgba_u16(p: &mut PipelineU16) { + let pixels = p.pixmap_dst.slice4_at_xy(p.dx, p.dy); + load_16161616(pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.r = mad(p.dr, inv(p.a), p.r); + p.g = mad(p.dg, inv(p.a), p.g); + p.b = mad(p.db, inv(p.a), p.b); + p.a = mad(p.da, inv(p.a), p.a); + store_16161616(&p.r, &p.g, &p.b, &p.a, pixels); + p.next_stage(); +} + +#[cfg(feature = "16bpc")] +pub fn source_over_rgba_tail_u16(p: &mut PipelineU16) { + let pixels = p.pixmap_dst.slice_at_xy(p.dx, p.dy); + load_16161616_tail(p.tail, pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); + p.r = mad(p.dr, inv(p.a), p.r); + p.g = mad(p.dg, inv(p.a), p.g); + p.b = mad(p.db, inv(p.a), p.b); + p.a = mad(p.da, inv(p.a), p.a); + store_16161616_tail(&p.r, &p.g, &p.b, &p.a, p.tail, pixels); + p.next_stage(); +} +stage_fn!(fn move_source_to_destination, move_source_to_destination_u16, |p| { p.dr = p.r; p.dg = p.g; p.db = p.b; p.da = p.a; p.next_stage(); -} +}); -fn premultiply(p: &mut Pipeline) { +stage_fn!(fn premultiply, premultiply_u16, |p| { p.r *= p.a; p.g *= p.a; p.b *= p.a; p.next_stage(); -} +}); -fn move_destination_to_source(p: &mut Pipeline) { +stage_fn!(fn move_destination_to_source, move_destination_to_source_u16, |p| { p.r = p.dr; p.g = p.dg; p.b = p.db; p.a = p.da; p.next_stage(); -} +}); -fn clamp_0(p: &mut Pipeline) { +stage_fn!(fn clamp_0, clamp_0_u16, |p| { p.r = p.r.max(f32x8::default()); p.g = p.g.max(f32x8::default()); p.b = p.b.max(f32x8::default()); p.a = p.a.max(f32x8::default()); p.next_stage(); -} +}); -fn clamp_a(p: &mut Pipeline) { +stage_fn!(fn clamp_a, clamp_a_u16, |p| { p.r = p.r.min(f32x8::splat(1.0)); p.g = p.g.min(f32x8::splat(1.0)); p.b = p.b.min(f32x8::splat(1.0)); p.a = p.a.min(f32x8::splat(1.0)); p.next_stage(); -} +}); -fn uniform_color(p: &mut Pipeline) { +stage_fn!(fn uniform_color, uniform_color_u16, |p| { let ctx = &p.ctx.uniform_color; p.r = f32x8::splat(ctx.r); p.g = f32x8::splat(ctx.g); @@ -251,9 +680,9 @@ fn uniform_color(p: &mut Pipeline) { p.a = f32x8::splat(ctx.a); p.next_stage(); -} +}); -fn seed_shader(p: &mut Pipeline) { +stage_fn!(fn seed_shader, seed_shader_u16, |p| { let iota = f32x8::from([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5]); p.r = f32x8::splat(p.dx as f32) + iota; @@ -267,51 +696,26 @@ fn seed_shader(p: &mut Pipeline) { p.da = f32x8::default(); p.next_stage(); -} +}); + + + + -pub fn load_dst(p: &mut Pipeline) { - load_8888(p.pixmap_dst.slice4_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); - p.next_stage(); -} -pub fn load_dst_tail(p: &mut Pipeline) { - load_8888_tail(p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy), &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); - p.next_stage(); -} -pub fn store(p: &mut Pipeline) { - store_8888(&p.r, &p.g, &p.b, &p.a, p.pixmap_dst.slice4_at_xy(p.dx, p.dy)); - p.next_stage(); -} -pub fn store_tail(p: &mut Pipeline) { - store_8888_tail(&p.r, &p.g, &p.b, &p.a, p.tail, p.pixmap_dst.slice_at_xy(p.dx, p.dy)); - p.next_stage(); -} // Currently, all mask/A8 pixmaps are handled by lowp. -pub fn load_dst_u8(_: &mut Pipeline) { - // unreachable -} -pub fn load_dst_u8_tail(_: &mut Pipeline) { - // unreachable -} -pub fn store_u8(_: &mut Pipeline) { - // unreachable -} -pub fn store_u8_tail(_: &mut Pipeline) { - // unreachable -} -pub fn gather(p: &mut Pipeline) { - let ix = gather_ix(p.pixmap_src, p.r, p.g); - load_8888(&p.pixmap_src.gather(ix), &mut p.r, &mut p.g, &mut p.b, &mut p.a); - p.next_stage(); -} + + + + #[inline(always)] fn gather_ix(pixmap: PixmapRef, mut x: f32x8, mut y: f32x8) -> u32x8 { @@ -330,11 +734,23 @@ fn ulp_sub(v: f32) -> f32 { bytemuck::cast::(bytemuck::cast::(v) - 1) } -fn load_mask_u8(_: &mut Pipeline) { - // unreachable -} +stage_fn!(fn load_mask_u8, load_mask_u8_u16, |p| { + let offset = p.mask_ctx.offset(p.dx, p.dy); + let mut c = [0.0; 8]; + for i in 0..p.tail { + c[i] = p.mask_ctx.data[offset + i] as f32; + } + let c = f32x8::from(c) / f32x8::splat(255.0); + + p.r = f32x8::default(); + p.g = f32x8::default(); + p.b = f32x8::default(); + p.a = c; + + p.next_stage(); +}); -fn mask_u8(p: &mut Pipeline) { +stage_fn!(fn mask_u8, mask_u8_u16, |p| { let offset = p.mask_ctx.offset(p.dx, p.dy); let mut c = [0.0; 8]; for i in 0..p.tail { @@ -352,9 +768,9 @@ fn mask_u8(p: &mut Pipeline) { p.a *= c; p.next_stage(); -} +}); -fn scale_u8(p: &mut Pipeline) { +stage_fn!(fn scale_u8, scale_u8_u16, |p| { // Load u8xTail and cast it to f32x8. let data = p.aa_mask_ctx.copy_at_xy(p.dx, p.dy, p.tail); let c = f32x8::from([data[0] as f32, data[1] as f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); @@ -366,9 +782,9 @@ fn scale_u8(p: &mut Pipeline) { p.a *= c; p.next_stage(); -} +}); -fn lerp_u8(p: &mut Pipeline) { +stage_fn!(fn lerp_u8, lerp_u8_u16, |p| { // Load u8xTail and cast it to f32x8. let data = p.aa_mask_ctx.copy_at_xy(p.dx, p.dy, p.tail); let c = f32x8::from([data[0] as f32, data[1] as f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]); @@ -380,9 +796,9 @@ fn lerp_u8(p: &mut Pipeline) { p.a = lerp(p.da, p.a, c); p.next_stage(); -} +}); -fn scale_1_float(p: &mut Pipeline) { +stage_fn!(fn scale_1_float, scale_1_float_u16, |p| { let c = f32x8::splat(p.ctx.current_coverage); p.r *= c; p.g *= c; @@ -390,9 +806,9 @@ fn scale_1_float(p: &mut Pipeline) { p.a *= c; p.next_stage(); -} +}); -fn lerp_1_float(p: &mut Pipeline) { +stage_fn!(fn lerp_1_float, lerp_1_float_u16, |p| { let c = f32x8::splat(p.ctx.current_coverage); p.r = lerp(p.dr, p.r, c); p.g = lerp(p.dg, p.g, c); @@ -400,10 +816,10 @@ fn lerp_1_float(p: &mut Pipeline) { p.a = lerp(p.da, p.a, c); p.next_stage(); -} +}); macro_rules! blend_fn { - ($name:ident, $f:expr) => { + ($name:ident, $name_u16:ident, $f:expr) => { fn $name(p: &mut Pipeline) { p.r = $f(p.r, p.dr, p.a, p.da); p.g = $f(p.g, p.dg, p.a, p.da); @@ -412,28 +828,38 @@ macro_rules! blend_fn { p.next_stage(); } + + #[cfg(feature = "16bpc")] + fn $name_u16(p: &mut PipelineU16) { + p.r = $f(p.r, p.dr, p.a, p.da); + p.g = $f(p.g, p.dg, p.a, p.da); + p.b = $f(p.b, p.db, p.a, p.da); + p.a = $f(p.a, p.da, p.a, p.da); + + p.next_stage(); + } }; } -blend_fn!(clear, |_, _, _, _| f32x8::default()); -blend_fn!(source_atop, |s, d, sa, da| s * da + d * inv(sa)); -blend_fn!(destination_atop, |s, d, sa, da| d * sa + s * inv(da)); -blend_fn!(source_in, |s, _, _, da| s * da); -blend_fn!(destination_in, |_, d, sa, _| d * sa); -blend_fn!(source_out, |s, _, _, da| s * inv(da)); -blend_fn!(destination_out, |_, d, sa, _| d * inv(sa)); -blend_fn!(source_over, |s, d, sa, _| mad(d, inv(sa), s)); -blend_fn!(destination_over, |s, d, _, da| mad(s, inv(da), d)); -blend_fn!(modulate, |s, d, _, _| s * d); -blend_fn!(multiply, |s, d, sa, da| s * inv(da) + d * inv(sa) + s * d); -blend_fn!(screen, |s, d, _, _| s + d - s * d); -blend_fn!(xor, |s, d, sa, da| s * inv(da) + d * inv(sa)); +blend_fn!(clear, clear_u16, |_, _, _, _| f32x8::default()); +blend_fn!(source_atop, source_atop_u16, |s, d, sa, da| s * da + d * inv(sa)); +blend_fn!(destination_atop, destination_atop_u16, |s, d, sa, da| d * sa + s * inv(da)); +blend_fn!(source_in, source_in_u16, |s, _, _, da| s * da); +blend_fn!(destination_in, destination_in_u16, |_, d, sa, _| d * sa); +blend_fn!(source_out, source_out_u16, |s, _, _, da| s * inv(da)); +blend_fn!(destination_out, destination_out_u16, |_, d, sa, _| d * inv(sa)); +blend_fn!(source_over, source_over_u16, |s, d, sa, _| mad(d, inv(sa), s)); +blend_fn!(destination_over, destination_over_u16, |s, d, _, da| mad(s, inv(da), d)); +blend_fn!(modulate, modulate_u16, |s, d, _, _| s * d); +blend_fn!(multiply, multiply_u16, |s, d, sa, da| s * inv(da) + d * inv(sa) + s * d); +blend_fn!(screen, screen_u16, |s, d, _, _| s + d - s * d); +blend_fn!(xor, xor_u16, |s, d, sa, da| s * inv(da) + d * inv(sa)); // Wants a type for some reason. -blend_fn!(plus, |s: f32x8, d: f32x8, _, _| (s + d).min(f32x8::splat(1.0))); +blend_fn!(plus, plus_u16, |s: f32x8, d: f32x8, _, _| (s + d).min(f32x8::splat(1.0))); macro_rules! blend_fn2 { - ($name:ident, $f:expr) => { + ($name:ident, $name_u16:ident, $f:expr) => { fn $name(p: &mut Pipeline) { // The same logic applied to color, and source_over for alpha. p.r = $f(p.r, p.dr, p.a, p.da); @@ -443,15 +869,25 @@ macro_rules! blend_fn2 { p.next_stage(); } + + #[cfg(feature = "16bpc")] + fn $name_u16(p: &mut PipelineU16) { + p.r = $f(p.r, p.dr, p.a, p.da); + p.g = $f(p.g, p.dg, p.a, p.da); + p.b = $f(p.b, p.db, p.a, p.da); + p.a = mad(p.da, inv(p.a), p.a); + + p.next_stage(); + } }; } -blend_fn2!(darken, |s: f32x8, d, sa, da: f32x8| s + d - (s * da).max(d * sa)); -blend_fn2!(lighten, |s: f32x8, d, sa, da: f32x8| s + d - (s * da).min(d * sa)); -blend_fn2!(difference, |s: f32x8, d, sa, da: f32x8| s + d - two((s * da).min(d * sa))); -blend_fn2!(exclusion, |s: f32x8, d, _, _| s + d - two(s * d)); +blend_fn2!(darken, darken_u16, |s: f32x8, d, sa, da: f32x8| s + d - (s * da).max(d * sa)); +blend_fn2!(lighten, lighten_u16, |s: f32x8, d, sa, da: f32x8| s + d - (s * da).min(d * sa)); +blend_fn2!(difference, difference_u16, |s: f32x8, d, sa, da: f32x8| s + d - two((s * da).min(d * sa))); +blend_fn2!(exclusion, exclusion_u16, |s: f32x8, d, _, _| s + d - two(s * d)); -blend_fn2!(color_burn, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| +blend_fn2!(color_burn, color_burn_u16, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| d.cmp_eq(da).blend( d + s * inv(da), s.cmp_eq(f32x8::default()).blend( @@ -461,7 +897,7 @@ blend_fn2!(color_burn, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| ) ); -blend_fn2!(color_dodge, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| +blend_fn2!(color_dodge, color_dodge_u16, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| d.cmp_eq(f32x8::default()).blend( s * inv(da), s.cmp_eq(sa).blend( @@ -471,21 +907,21 @@ blend_fn2!(color_dodge, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| ) ); -blend_fn2!(hard_light, |s: f32x8, d: f32x8, sa, da| +blend_fn2!(hard_light, hard_light_u16, |s: f32x8, d: f32x8, sa, da| s * inv(da) + d * inv(sa) + two(s).cmp_le(sa).blend( two(s * d), sa * da - two((da - d) * (sa - s)) ) ); -blend_fn2!(overlay, |s: f32x8, d: f32x8, sa, da| +blend_fn2!(overlay, overlay_u16, |s: f32x8, d: f32x8, sa, da| s * inv(da) + d * inv(sa) + two(d).cmp_le(da).blend( two(s * d), sa * da - two((da - d) * (sa - s)) ) ); -blend_fn2!(soft_light, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| { +blend_fn2!(soft_light, soft_light_u16, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| { let m = da.cmp_gt(f32x8::default()).blend(d / da, f32x8::default()); let s2 = two(s); let m4 = two(two(m)); @@ -512,7 +948,7 @@ blend_fn2!(soft_light, |s: f32x8, d: f32x8, sa: f32x8, da: f32x8| { // Anything extra we add beyond that is to make the math work with premul inputs. macro_rules! blend_fn3 { - ($name:ident, $f:expr) => { + ($name:ident, $name_u16:ident, $f:expr) => { fn $name(p: &mut Pipeline) { let (tr, tg, tb, ta) = $f(p.r, p.g, p.b, p.a, p.dr, p.dg, p.db, p.da); p.r = tr; @@ -522,10 +958,21 @@ macro_rules! blend_fn3 { p.next_stage(); } + + #[cfg(feature = "16bpc")] + fn $name_u16(p: &mut PipelineU16) { + let (tr, tg, tb, ta) = $f(p.r, p.g, p.b, p.a, p.dr, p.dg, p.db, p.da); + p.r = tr; + p.g = tg; + p.b = tb; + p.a = ta; + + p.next_stage(); + } }; } -blend_fn3!(hue, hue_k); +blend_fn3!(hue, hue_u16, hue_k); #[inline(always)] fn hue_k( @@ -548,7 +995,7 @@ fn hue_k( (r, g, b, a) } -blend_fn3!(saturation, saturation_k); +blend_fn3!(saturation, saturation_u16, saturation_k); #[inline(always)] fn saturation_k( @@ -571,7 +1018,7 @@ fn saturation_k( (r, g, b, a) } -blend_fn3!(color, color_k); +blend_fn3!(color, color_u16, color_k); #[inline(always)] fn color_k( @@ -593,7 +1040,7 @@ fn color_k( (r, g, b, a) } -blend_fn3!(luminosity, luminosity_k); +blend_fn3!(luminosity, luminosity_u16, luminosity_k); #[inline(always)] fn luminosity_k( @@ -657,40 +1104,20 @@ fn clip_color(r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: f32x8) { let clip = |mut c| { c = mx.cmp_ge(f32x8::default()).blend(c, l + (c - l) * l / (l - mn)); c = mx.cmp_gt(a).blend(l + (c - l) * (a - l) / (mx - l), c); - c = c.max(f32x8::default()); // Sometimes without this we may dip just a little negative. - c - }; - - *r = clip(*r); - *g = clip(*g); - *b = clip(*b); -} - -pub fn source_over_rgba(p: &mut Pipeline) { - let pixels = p.pixmap_dst.slice4_at_xy(p.dx, p.dy); - load_8888(pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); - p.r = mad(p.dr, inv(p.a), p.r); - p.g = mad(p.dg, inv(p.a), p.g); - p.b = mad(p.db, inv(p.a), p.b); - p.a = mad(p.da, inv(p.a), p.a); - store_8888(&p.r, &p.g, &p.b, &p.a, pixels); - - p.next_stage(); -} - -pub fn source_over_rgba_tail(p: &mut Pipeline) { - let pixels = p.pixmap_dst.slice_at_xy(p.dx, p.dy); - load_8888_tail(p.tail, pixels, &mut p.dr, &mut p.dg, &mut p.db, &mut p.da); - p.r = mad(p.dr, inv(p.a), p.r); - p.g = mad(p.dg, inv(p.a), p.g); - p.b = mad(p.db, inv(p.a), p.b); - p.a = mad(p.da, inv(p.a), p.a); - store_8888_tail(&p.r, &p.g, &p.b, &p.a, p.tail, pixels); + c = c.max(f32x8::default()); // Sometimes without this we may dip just a little negative. + c + }; - p.next_stage(); + *r = clip(*r); + *g = clip(*g); + *b = clip(*b); } -fn transform(p: &mut Pipeline) { + + + + +stage_fn!(fn transform, transform_u16, |p| { let ts = &p.ctx.transform; let tr = mad(p.r, f32x8::splat(ts.sx), mad(p.g, f32x8::splat(ts.kx), f32x8::splat(ts.tx))); @@ -699,13 +1126,13 @@ fn transform(p: &mut Pipeline) { p.g = tg; p.next_stage(); -} +}); // Tile x or y to [0,limit) == [0,limit - 1 ulp] (think, sampling from images). // The gather stages will hard clamp the output of these stages to [0,limit)... // we just need to do the basic repeat or mirroring. -fn reflect(p: &mut Pipeline) { +stage_fn!(fn reflect, reflect_u16, |p| { let ctx = &p.ctx.limit_x; p.r = exclusive_reflect(p.r, ctx.scale, ctx.inv_scale); @@ -713,7 +1140,7 @@ fn reflect(p: &mut Pipeline) { p.g = exclusive_reflect(p.g, ctx.scale, ctx.inv_scale); p.next_stage(); -} +}); #[inline(always)] fn exclusive_reflect(v: f32x8, limit: f32, inv_limit: f32) -> f32x8 { @@ -723,7 +1150,7 @@ fn exclusive_reflect(v: f32x8, limit: f32, inv_limit: f32) -> f32x8 { * ((v - limit) * (inv_limit * f32x8::splat(0.5))).floor() - limit).abs() } -fn repeat(p: &mut Pipeline) { +stage_fn!(fn repeat, repeat_u16, |p| { let ctx = &p.ctx.limit_x; p.r = exclusive_repeat(p.r, ctx.scale, ctx.inv_scale); @@ -731,40 +1158,16 @@ fn repeat(p: &mut Pipeline) { p.g = exclusive_repeat(p.g, ctx.scale, ctx.inv_scale); p.next_stage(); -} +}); #[inline(always)] fn exclusive_repeat(v: f32x8, limit: f32, inv_limit: f32) -> f32x8 { v - (v * f32x8::splat(inv_limit)).floor() * f32x8::splat(limit) } -fn bilinear(p: &mut Pipeline) { - let x = p.r; - let fx = (x + f32x8::splat(0.5)).fract(); - let y = p.g; - let fy = (y + f32x8::splat(0.5)).fract(); - let one = f32x8::splat(1.0); - let wx = [one - fx, fx]; - let wy = [one - fy, fy]; - - sampler_2x2(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); - - p.next_stage(); -} -fn bicubic(p: &mut Pipeline) { - let x = p.r; - let fx = (x + f32x8::splat(0.5)).fract(); - let y = p.g; - let fy = (y + f32x8::splat(0.5)).fract(); - let one = f32x8::splat(1.0); - let wx = [bicubic_far(one - fx), bicubic_near(one - fx), bicubic_near(fx), bicubic_far(fx)]; - let wy = [bicubic_far(one - fy), bicubic_near(one - fy), bicubic_near(fy), bicubic_far(fy)]; - sampler_4x4(p.pixmap_src, &p.ctx.sampler, x, y, &wx, &wy, &mut p.r, &mut p.g, &mut p.b, &mut p.a); - p.next_stage(); -} // In bicubic interpolation, the 16 pixels and +/- 0.5 and +/- 1.5 offsets from the sample // pixel center are combined with a non-uniform cubic filter, with higher values near the center. @@ -891,13 +1294,13 @@ fn tile(v: f32x8, mode: SpreadMode, limit: f32, inv_limit: f32) -> f32x8 { } } -fn pad_x1(p: &mut Pipeline) { +stage_fn!(fn pad_x1, pad_x1_u16, |p| { p.r = p.r.normalize(); p.next_stage(); -} +}); -fn reflect_x1(p: &mut Pipeline) { +stage_fn!(fn reflect_x1, reflect_x1_u16, |p| { p.r = ( (p.r - f32x8::splat(1.0)) - two(((p.r - f32x8::splat(1.0)) * f32x8::splat(0.5)).floor()) @@ -905,15 +1308,15 @@ fn reflect_x1(p: &mut Pipeline) { ).abs().normalize(); p.next_stage(); -} +}); -fn repeat_x1(p: &mut Pipeline) { +stage_fn!(fn repeat_x1, repeat_x1_u16, |p| { p.r = (p.r - p.r.floor()).normalize(); p.next_stage(); -} +}); -fn gradient(p: &mut Pipeline) { +stage_fn!(fn gradient, gradient_u16, |p| { let ctx = &p.ctx.gradient; // N.B. The loop starts at 1 because idx 0 is the color to use before the first stop. @@ -936,7 +1339,7 @@ fn gradient(p: &mut Pipeline) { gradient_lookup(ctx, &idx, p.r, &mut p.r, &mut p.g, &mut p.b, &mut p.a); p.next_stage(); -} +}); fn gradient_lookup( ctx: &super::GradientCtx, idx: &u32x8, t: f32x8, @@ -977,7 +1380,7 @@ fn gradient_lookup( *a = mad(t, fa, ba); } -fn evenly_spaced_2_stop_gradient(p: &mut Pipeline) { +stage_fn!(fn evenly_spaced_2_stop_gradient, evenly_spaced_2_stop_gradient_u16, |p| { let ctx = &p.ctx.evenly_spaced_2_stop_gradient; let t = p.r; @@ -987,9 +1390,9 @@ fn evenly_spaced_2_stop_gradient(p: &mut Pipeline) { p.a = mad(t, f32x8::splat(ctx.factor.a), f32x8::splat(ctx.bias.a)); p.next_stage(); -} +}); -fn xy_to_unit_angle(p: &mut Pipeline) { +stage_fn!(fn xy_to_unit_angle, xy_to_unit_angle_u16, |p| { let x = p.r; let y = p.g; let x_abs = x.abs(); @@ -1015,25 +1418,25 @@ fn xy_to_unit_angle(p: &mut Pipeline) { let phi = phi.cmp_ne(phi).blend(f32x8::splat(0.0), phi); p.r = phi; p.next_stage(); -} +}); -fn xy_to_radius(p: &mut Pipeline) { +stage_fn!(fn xy_to_radius, xy_to_radius_u16, |p| { let x2 = p.r * p.r; let y2 = p.g * p.g; p.r = (x2 + y2).sqrt(); p.next_stage(); -} +}); -fn xy_to_2pt_conical_focal_on_circle(p: &mut Pipeline) { +stage_fn!(fn xy_to_2pt_conical_focal_on_circle, xy_to_2pt_conical_focal_on_circle_u16, |p| { let x = p.r; let y = p.g; p.r = x + y * y / x; p.next_stage(); -} +}); -fn xy_to_2pt_conical_well_behaved(p: &mut Pipeline) { +stage_fn!(fn xy_to_2pt_conical_well_behaved, xy_to_2pt_conical_well_behaved_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; let x = p.r; @@ -1041,9 +1444,9 @@ fn xy_to_2pt_conical_well_behaved(p: &mut Pipeline) { p.r = (x * x + y * y).sqrt() - x * f32x8::splat(ctx.p0); p.next_stage(); -} +}); -fn xy_to_2pt_conical_greater(p: &mut Pipeline) { +stage_fn!(fn xy_to_2pt_conical_greater, xy_to_2pt_conical_greater_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; let x = p.r; @@ -1051,9 +1454,9 @@ fn xy_to_2pt_conical_greater(p: &mut Pipeline) { p.r = (x * x - y * y).sqrt() - x * f32x8::splat(ctx.p0); p.next_stage(); -} +}); -fn xy_to_2pt_conical_smaller(p: &mut Pipeline) { +stage_fn!(fn xy_to_2pt_conical_smaller, xy_to_2pt_conical_smaller_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; let x = p.r; @@ -1061,9 +1464,9 @@ fn xy_to_2pt_conical_smaller(p: &mut Pipeline) { p.r = -(x * x - y * y).sqrt() - x * f32x8::splat(ctx.p0); p.next_stage(); -} +}); -fn xy_to_2pt_conical_strip(p: &mut Pipeline) { +stage_fn!(fn xy_to_2pt_conical_strip, xy_to_2pt_conical_strip_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; let x = p.r; @@ -1071,9 +1474,9 @@ fn xy_to_2pt_conical_strip(p: &mut Pipeline) { p.r = x + (f32x8::splat(ctx.p0) - y * y).sqrt(); p.next_stage(); -} +}); -fn mask_2pt_conical_nan(p: &mut Pipeline) { +stage_fn!(fn mask_2pt_conical_nan, mask_2pt_conical_nan_u16, |p| { let ctx = &mut p.ctx.two_point_conical_gradient; let t = p.r; @@ -1082,9 +1485,9 @@ fn mask_2pt_conical_nan(p: &mut Pipeline) { ctx.mask = cond_to_mask(!is_degenerate.to_u32x8_bitcast()); p.next_stage(); -} +}); -fn mask_2pt_conical_degenerates(p: &mut Pipeline) { +stage_fn!(fn mask_2pt_conical_degenerates, mask_2pt_conical_degenerates_u16, |p| { let ctx = &mut p.ctx.two_point_conical_gradient; let t = p.r; @@ -1093,9 +1496,9 @@ fn mask_2pt_conical_degenerates(p: &mut Pipeline) { ctx.mask = cond_to_mask(!is_degenerate.to_u32x8_bitcast()); p.next_stage(); -} +}); -fn apply_vector_mask(p: &mut Pipeline) { +stage_fn!(fn apply_vector_mask, apply_vector_mask_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; p.r = (p.r.to_u32x8_bitcast() & ctx.mask).to_f32x8_bitcast(); @@ -1104,29 +1507,29 @@ fn apply_vector_mask(p: &mut Pipeline) { p.a = (p.a.to_u32x8_bitcast() & ctx.mask).to_f32x8_bitcast(); p.next_stage(); -} +}); -fn alter_2pt_conical_compensate_focal(p: &mut Pipeline) { +stage_fn!(fn alter_2pt_conical_compensate_focal, alter_2pt_conical_compensate_focal_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; p.r = p.r + f32x8::splat(ctx.p1); p.next_stage(); -} +}); -fn alter_2pt_conical_unswap(p: &mut Pipeline) { +stage_fn!(fn alter_2pt_conical_unswap, alter_2pt_conical_unswap_u16, |p| { p.r = f32x8::splat(1.0) - p.r; p.next_stage(); -} +}); -fn negate_x(p: &mut Pipeline) { +stage_fn!(fn negate_x, negate_x_u16, |p| { p.r = -p.r; p.next_stage(); -} +}); -fn apply_concentric_scale_bias(p: &mut Pipeline) { +stage_fn!(fn apply_concentric_scale_bias, apply_concentric_scale_bias_u16, |p| { let ctx = &p.ctx.two_point_conical_gradient; // Apply t = t * scale + bias for concentric gradients @@ -1134,55 +1537,55 @@ fn apply_concentric_scale_bias(p: &mut Pipeline) { p.r = x * f32x8::splat(ctx.p0) + f32x8::splat(ctx.p1); p.next_stage(); -} +}); -fn gamma_expand_2(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_2, gamma_expand_2_u16, |p| { p.r = p.r * p.r; p.g = p.g * p.g; p.b = p.b * p.b; p.next_stage(); -} +}); -fn gamma_expand_dst_2(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_dst_2, gamma_expand_dst_2_u16, |p| { p.dr = p.dr * p.dr; p.dg = p.dg * p.dg; p.db = p.db * p.db; p.next_stage(); -} +}); -fn gamma_compress_2(p: &mut Pipeline) { +stage_fn!(fn gamma_compress_2, gamma_compress_2_u16, |p| { p.r = p.r.sqrt(); p.g = p.g.sqrt(); p.b = p.b.sqrt(); p.next_stage(); -} +}); -fn gamma_expand_22(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_22, gamma_expand_22_u16, |p| { p.r = p.r.powf(2.2); p.g = p.g.powf(2.2); p.b = p.b.powf(2.2); p.next_stage(); -} +}); -fn gamma_expand_dst_22(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_dst_22, gamma_expand_dst_22_u16, |p| { p.dr = p.dr.powf(2.2); p.dg = p.dg.powf(2.2); p.db = p.db.powf(2.2); p.next_stage(); -} +}); -fn gamma_compress_22(p: &mut Pipeline) { +stage_fn!(fn gamma_compress_22, gamma_compress_22_u16, |p| { p.r = p.r.powf(0.45454545); p.g = p.g.powf(0.45454545); p.b = p.b.powf(0.45454545); p.next_stage(); -} +}); fn srgb_expand(x: f32x8) -> f32x8 { let small = x.cmp_le(f32x8::splat(0.04045)); @@ -1198,33 +1601,33 @@ fn srgb_compress(x: f32x8) -> f32x8 { small.blend(linear, exp) } -fn gamma_expand_srgb(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_srgb, gamma_expand_srgb_u16, |p| { p.r = srgb_expand(p.r); p.g = srgb_expand(p.g); p.b = srgb_expand(p.b); p.next_stage(); -} +}); -fn gamma_expand_dst_srgb(p: &mut Pipeline) { +stage_fn!(fn gamma_expand_dst_srgb, gamma_expand_dst_srgb_u16, |p| { p.dr = srgb_expand(p.dr); p.dg = srgb_expand(p.dg); p.db = srgb_expand(p.db); p.next_stage(); -} +}); -fn gamma_compress_srgb(p: &mut Pipeline) { +stage_fn!(fn gamma_compress_srgb, gamma_compress_srgb_u16, |p| { p.r = srgb_compress(p.r); p.g = srgb_compress(p.g); p.b = srgb_compress(p.b); p.next_stage(); -} +}); -pub fn just_return(_: &mut Pipeline) { +stage_fn!(pub fn just_return, just_return_u16, |p| { // Ends the loop. -} +}); #[inline(always)] fn cond_to_mask(cond: u32x8) -> u32x8 { @@ -1362,3 +1765,206 @@ fn mad(f: f32x8, m: f32x8, a: f32x8) -> f32x8 { fn lerp(from: f32x8, to: f32x8, t: f32x8) -> f32x8 { mad(to - from, t, from) } + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn gather_ix_dyn(pixmap: DynamicPixmapRef, mut x: f32x8, mut y: f32x8) -> u32x8 { + let w = ulp_sub(pixmap.width() as f32); + let h = ulp_sub(pixmap.height() as f32); + x = x.max(f32x8::default()).min(f32x8::splat(w)); + y = y.max(f32x8::default()).min(f32x8::splat(h)); + (y.trunc_int() * i32x8::splat(pixmap.width() as i32) + x.trunc_int()).to_u32x8_bitcast() +} + +#[cfg(feature = "16bpc")] +fn sampler_2x2_dyn( + pixmap: DynamicPixmapRef, + ctx: &super::SamplerCtx, + cx: f32x8, cy: f32x8, + wx: &[f32x8; 2], wy: &[f32x8; 2], + r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: &mut f32x8, +) { + *r = f32x8::default(); + *g = f32x8::default(); + *b = f32x8::default(); + *a = f32x8::default(); + + let one = f32x8::splat(1.0); + let start = -0.5; + let mut y = cy + f32x8::splat(start); + for j in 0..2 { + let mut x = cx + f32x8::splat(start); + for i in 0..2 { + let mut rr = f32x8::default(); + let mut gg = f32x8::default(); + let mut bb = f32x8::default(); + let mut aa = f32x8::default(); + sample_dyn(pixmap, ctx, x, y, &mut rr, &mut gg, &mut bb, &mut aa); + + let w = wx[i] * wy[j]; + *r = mad(w, rr, *r); + *g = mad(w, gg, *g); + *b = mad(w, bb, *b); + *a = mad(w, aa, *a); + + x += one; + } + + y += one; + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn sampler_4x4_dyn( + pixmap: DynamicPixmapRef, + ctx: &super::SamplerCtx, + cx: f32x8, cy: f32x8, + wx: &[f32x8; 4], wy: &[f32x8; 4], + r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: &mut f32x8, +) { + *r = f32x8::default(); + *g = f32x8::default(); + *b = f32x8::default(); + *a = f32x8::default(); + + let one = f32x8::splat(1.0); + let start = -1.5; + let mut y = cy + f32x8::splat(start); + for j in 0..4 { + let mut x = cx + f32x8::splat(start); + for i in 0..4 { + let mut rr = f32x8::default(); + let mut gg = f32x8::default(); + let mut bb = f32x8::default(); + let mut aa = f32x8::default(); + sample_dyn(pixmap, ctx, x, y, &mut rr, &mut gg, &mut bb, &mut aa); + + let w = wx[i] * wy[j]; + *r = mad(w, rr, *r); + *g = mad(w, gg, *g); + *b = mad(w, bb, *b); + *a = mad(w, aa, *a); + + x += one; + } + + y += one; + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn sample_dyn( + pixmap: DynamicPixmapRef, ctx: &super::SamplerCtx, mut x: f32x8, mut y: f32x8, + r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: &mut f32x8, +) { + x = tile(x, ctx.spread_mode, pixmap.width() as f32, ctx.inv_width); + y = tile(y, ctx.spread_mode, pixmap.height() as f32, ctx.inv_height); + + let ix = gather_ix_dyn(pixmap, x, y); + match pixmap { + DynamicPixmapRef::U8(src) => load_8888(&src.gather(ix), r, g, b, a), + DynamicPixmapRef::U16(src) => load_16161616(&src.gather(ix), r, g, b, a), + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn load_16161616( + data: &[PremultipliedColorU16; STAGE_WIDTH], + r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: &mut f32x8, +) { + const FACTOR: f32 = 1.0 / 65535.0; + + *r = f32x8::from([ + data[0].red() as f32 * FACTOR, data[1].red() as f32 * FACTOR, + data[2].red() as f32 * FACTOR, data[3].red() as f32 * FACTOR, + data[4].red() as f32 * FACTOR, data[5].red() as f32 * FACTOR, + data[6].red() as f32 * FACTOR, data[7].red() as f32 * FACTOR, + ]); + + *g = f32x8::from([ + data[0].green() as f32 * FACTOR, data[1].green() as f32 * FACTOR, + data[2].green() as f32 * FACTOR, data[3].green() as f32 * FACTOR, + data[4].green() as f32 * FACTOR, data[5].green() as f32 * FACTOR, + data[6].green() as f32 * FACTOR, data[7].green() as f32 * FACTOR, + ]); + + *b = f32x8::from([ + data[0].blue() as f32 * FACTOR, data[1].blue() as f32 * FACTOR, + data[2].blue() as f32 * FACTOR, data[3].blue() as f32 * FACTOR, + data[4].blue() as f32 * FACTOR, data[5].blue() as f32 * FACTOR, + data[6].blue() as f32 * FACTOR, data[7].blue() as f32 * FACTOR, + ]); + + *a = f32x8::from([ + data[0].alpha() as f32 * FACTOR, data[1].alpha() as f32 * FACTOR, + data[2].alpha() as f32 * FACTOR, data[3].alpha() as f32 * FACTOR, + data[4].alpha() as f32 * FACTOR, data[5].alpha() as f32 * FACTOR, + data[6].alpha() as f32 * FACTOR, data[7].alpha() as f32 * FACTOR, + ]); +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn load_16161616_tail( + tail: usize, data: &[PremultipliedColorU16], + r: &mut f32x8, g: &mut f32x8, b: &mut f32x8, a: &mut f32x8, +) { + let mut tmp = [PremultipliedColorU16::TRANSPARENT; STAGE_WIDTH]; + tmp[0..tail].copy_from_slice(&data[0..tail]); + load_16161616(&tmp, r, g, b, a); +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn store_16161616( + r: &f32x8, g: &f32x8, b: &f32x8, a: &f32x8, + data: &mut [PremultipliedColorU16; STAGE_WIDTH], +) { + let r: [i32; 8] = unnorm_u16(r).into(); + let g: [i32; 8] = unnorm_u16(g).into(); + let b: [i32; 8] = unnorm_u16(b).into(); + let a: [i32; 8] = unnorm_u16(a).into(); + + let conv = |rr, gg, bb, aa| + PremultipliedColorU16::from_rgba_unchecked(rr as u16, gg as u16, bb as u16, aa as u16); + + data[0] = conv(r[0], g[0], b[0], a[0]); + data[1] = conv(r[1], g[1], b[1], a[1]); + data[2] = conv(r[2], g[2], b[2], a[2]); + data[3] = conv(r[3], g[3], b[3], a[3]); + data[4] = conv(r[4], g[4], b[4], a[4]); + data[5] = conv(r[5], g[5], b[5], a[5]); + data[6] = conv(r[6], g[6], b[6], a[6]); + data[7] = conv(r[7], g[7], b[7], a[7]); +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn store_16161616_tail( + r: &f32x8, g: &f32x8, b: &f32x8, a: &f32x8, + tail: usize, data: &mut [PremultipliedColorU16], +) { + let r: [i32; 8] = unnorm_u16(r).into(); + let g: [i32; 8] = unnorm_u16(g).into(); + let b: [i32; 8] = unnorm_u16(b).into(); + let a: [i32; 8] = unnorm_u16(a).into(); + + for i in 0..STAGE_WIDTH { + data[i] = PremultipliedColorU16::from_rgba_unchecked( + r[i] as u16, g[i] as u16, b[i] as u16, a[i] as u16, + ); + + if i + 1 == tail { + break; + } + } +} + +#[cfg(feature = "16bpc")] +#[inline(always)] +fn unnorm_u16(v: &f32x8) -> i32x8 { + (v.max(f32x8::default()).min(f32x8::splat(1.0)) * f32x8::splat(65535.0)).round_int() +} diff --git a/src/pipeline/lowp.rs b/src/pipeline/lowp.rs index fc1a6c5..acb0e99 100644 --- a/src/pipeline/lowp.rs +++ b/src/pipeline/lowp.rs @@ -58,6 +58,17 @@ pub struct Pipeline<'a, 'b: 'a> { dy: usize, } +impl core::fmt::Debug for Pipeline<'_, '_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("Pipeline") + .field("index", &self.index) + .field("tail", &self.tail) + .field("dx", &self.dx) + .field("dy", &self.dy) + .finish() + } +} + impl Pipeline<'_, '_> { #[inline(always)] fn next_stage(&mut self) { diff --git a/src/pipeline/mod.rs b/src/pipeline/mod.rs index b3612ee..6cfed22 100644 --- a/src/pipeline/mod.rs +++ b/src/pipeline/mod.rs @@ -50,17 +50,21 @@ use arrayvec::ArrayVec; use tiny_skia_path::NormalizedF32; -use crate::{Color, PremultipliedColor, PremultipliedColorU8, SpreadMode}; -use crate::{PixmapRef, Transform}; +use crate::Transform; +use crate::{ + Color, DynamicPixmapRef, Pixel, PixmapRefGeneric, PremultipliedColor, PremultipliedColorU8, + SpreadMode, +}; pub use blitter::RasterPipelineBlitter; +pub use highp::HighPixel; use crate::geom::ScreenIntRect; -use crate::pixmap::SubPixmapMut; +use crate::pixmap::{SubPixmapMut, SubPixmapMutGeneric}; use crate::wide::u32x8; mod blitter; -#[rustfmt::skip] mod highp; +#[rustfmt::skip] pub mod highp; #[rustfmt::skip] mod lowp; const MAX_STAGES: usize = 32; // More than enough. @@ -151,9 +155,9 @@ pub enum Stage { pub const STAGES_COUNT: usize = Stage::GammaCompressSrgb as usize + 1; -impl PixmapRef<'_> { +impl PixmapRefGeneric<'_, P> { #[inline(always)] - pub(crate) fn gather(&self, index: u32x8) -> [PremultipliedColorU8; highp::STAGE_WIDTH] { + pub(crate) fn gather(&self, index: u32x8) -> [P; highp::STAGE_WIDTH] { let index: [u32; 8] = bytemuck::cast(index); let pixels = self.pixels(); [ @@ -169,42 +173,38 @@ impl PixmapRef<'_> { } } -impl SubPixmapMut<'_> { +impl SubPixmapMutGeneric<'_, P> { #[inline(always)] pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize { - self.real_width * dy + dx + (self.real_width * dy + dx) * P::BYTES_PER_PIXEL } #[inline(always)] - pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [PremultipliedColorU8] { + pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [P] { let offset = self.offset(dx, dy); - &mut self.pixels_mut()[offset..] + bytemuck::cast_slice_mut(&mut self.data[offset..]) } #[inline(always)] pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] { - let offset = self.offset(dx, dy); + let offset = self.real_width * dy + dx; &mut self.data[offset..] } #[inline(always)] - pub(crate) fn slice4_at_xy( - &mut self, - dx: usize, - dy: usize, - ) -> &mut [PremultipliedColorU8; highp::STAGE_WIDTH] { + pub(crate) fn slice4_at_xy(&mut self, dx: usize, dy: usize) -> &mut [P; highp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - self.pixels_mut()[offset..].first_chunk_mut().unwrap() + bytemuck::from_bytes_mut( + &mut self.data[offset..offset + P::BYTES_PER_PIXEL * highp::STAGE_WIDTH], + ) } #[inline(always)] - pub(crate) fn slice16_at_xy( - &mut self, - dx: usize, - dy: usize, - ) -> &mut [PremultipliedColorU8; lowp::STAGE_WIDTH] { + pub(crate) fn slice16_at_xy(&mut self, dx: usize, dy: usize) -> &mut [P; lowp::STAGE_WIDTH] { let offset = self.offset(dx, dy); - self.pixels_mut()[offset..].first_chunk_mut().unwrap() + bytemuck::from_bytes_mut( + &mut self.data[offset..offset + P::BYTES_PER_PIXEL * lowp::STAGE_WIDTH], + ) } #[inline(always)] @@ -213,12 +213,14 @@ impl SubPixmapMut<'_> { dx: usize, dy: usize, ) -> &mut [u8; lowp::STAGE_WIDTH] { - let offset = self.offset(dx, dy); - self.data[offset..].first_chunk_mut().unwrap() + let offset = self.real_width * dy + dx; + self.data[offset..offset + lowp::STAGE_WIDTH] + .first_chunk_mut() + .unwrap() } } -#[derive(Default, Debug)] +#[derive(Copy, Clone, Default, Debug)] pub struct AAMaskCtx { pub pixels: [u8; 2], pub stride: u32, // can be zero @@ -252,7 +254,7 @@ impl MaskCtx<'_> { } } -#[derive(Default)] +#[derive(Clone, Debug, Default)] pub struct Context { pub current_coverage: f32, pub sampler: SamplerCtx, @@ -395,35 +397,64 @@ impl RasterPipelineBuilder { self.ctx.uniform_color = ctx; } - pub fn compile(self) -> RasterPipeline { + pub fn compile(self) -> RasterPipelineGeneric

{ if self.stages.is_empty() { - return RasterPipeline { - kind: RasterPipelineKind::High { + return RasterPipelineGeneric { + kind: RasterPipelineKindGeneric::High { functions: ArrayVec::new(), tail_functions: ArrayVec::new(), }, ctx: Context::default(), }; } - let is_lowp_compatible = self .stages .iter() .all(|stage| !lowp::fn_ptr_eq(lowp::STAGES[*stage as usize], lowp::null_fn)); - if self.force_hq_pipeline || !is_lowp_compatible { + #[cfg(feature = "16bpc")] + if P::BYTES_PER_PIXEL == 8 { + let mut functions: ArrayVec<_, MAX_STAGES> = self + .stages + .iter() + .map(|stage| highp::STAGES_U16[*stage as usize]) + .collect(); + functions.push(highp::just_return_u16 as highp::StageFnU16); + + let mut tail_functions = functions.clone(); + for fun in &mut tail_functions { + if highp::fn_ptr_u16(*fun) == highp::fn_ptr_u16(highp::load_dst_u16) { + *fun = highp::load_dst_tail_u16 as highp::StageFnU16; + } else if highp::fn_ptr_u16(*fun) == highp::fn_ptr_u16(highp::store_u16) { + *fun = highp::store_tail_u16 as highp::StageFnU16; + } else if highp::fn_ptr_u16(*fun) == highp::fn_ptr_u16(highp::load_dst_u8_u16) { + *fun = highp::load_dst_u8_tail_u16 as highp::StageFnU16; + } else if highp::fn_ptr_u16(*fun) == highp::fn_ptr_u16(highp::store_u8_u16) { + *fun = highp::store_u8_tail_u16 as highp::StageFnU16; + } else if highp::fn_ptr_u16(*fun) == highp::fn_ptr_u16(highp::source_over_rgba_u16) + { + *fun = highp::source_over_rgba_tail_u16 as highp::StageFnU16; + } + } + + return RasterPipelineGeneric { + kind: RasterPipelineKindGeneric::HighU16 { + functions, + tail_functions, + }, + ctx: self.ctx, + }; + } + + let is_highp = self.force_hq_pipeline || !is_lowp_compatible; + if is_highp { let mut functions: ArrayVec<_, MAX_STAGES> = self .stages .iter() - .map(|stage| highp::STAGES[*stage as usize] as highp::StageFn) + .map(|stage| highp::STAGES[*stage as usize]) .collect(); functions.push(highp::just_return as highp::StageFn); - // I wasn't able to reproduce Skia's load_8888_/store_8888_ performance. - // Skia uses fallthrough switch, which is probably the reason. - // In Rust, any branching in load/store code drastically affects the performance. - // So instead, we're using two "programs": one for "full stages" and one for "tail stages". - // While the only difference is the load/store methods. let mut tail_functions = functions.clone(); for fun in &mut tail_functions { if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst) { @@ -435,14 +466,12 @@ impl RasterPipelineBuilder { } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store_u8) { *fun = highp::store_u8_tail as highp::StageFn; } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::source_over_rgba) { - // SourceOverRgba calls load/store manually, without the pipeline, - // therefore we have to switch it too. *fun = highp::source_over_rgba_tail as highp::StageFn; } } - RasterPipeline { - kind: RasterPipelineKind::High { + RasterPipelineGeneric { + kind: RasterPipelineKindGeneric::High { functions, tail_functions, }, @@ -456,7 +485,6 @@ impl RasterPipelineBuilder { .collect(); functions.push(lowp::just_return as lowp::StageFn); - // See above. let mut tail_functions = functions.clone(); for fun in &mut tail_functions { if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst) { @@ -468,14 +496,12 @@ impl RasterPipelineBuilder { } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store_u8) { *fun = lowp::store_u8_tail as lowp::StageFn; } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::source_over_rgba) { - // SourceOverRgba calls load/store manually, without the pipeline, - // therefore we have to switch it too. *fun = lowp::source_over_rgba_tail as lowp::StageFn; } } - RasterPipeline { - kind: RasterPipelineKind::Low { + RasterPipelineGeneric { + kind: RasterPipelineKindGeneric::Low { functions, tail_functions, }, @@ -485,37 +511,71 @@ impl RasterPipelineBuilder { } } -pub enum RasterPipelineKind { +#[allow(dead_code)] +pub type RasterPipeline = RasterPipelineGeneric; + +pub enum RasterPipelineKindGeneric { High { functions: ArrayVec, tail_functions: ArrayVec, }, + #[cfg(feature = "16bpc")] + HighU16 { + functions: ArrayVec, + tail_functions: ArrayVec, + }, Low { functions: ArrayVec, tail_functions: ArrayVec, }, + #[allow(dead_code)] + _Marker(core::marker::PhantomData

), } -pub struct RasterPipeline { - kind: RasterPipelineKind, +pub struct RasterPipelineGeneric { + kind: RasterPipelineKindGeneric

, pub ctx: Context, } -impl RasterPipeline { +impl RasterPipelineGeneric

{ pub fn run( &mut self, rect: &ScreenIntRect, aa_mask_ctx: AAMaskCtx, mask_ctx: MaskCtx, - pixmap_src: PixmapRef, - pixmap_dst: &mut SubPixmapMut, + pixmap_src: DynamicPixmapRef, + pixmap_dst: &mut SubPixmapMutGeneric<'_, P>, ) { match self.kind { - RasterPipelineKind::High { + RasterPipelineKindGeneric::High { ref functions, ref tail_functions, } => { + #[allow(clippy::infallible_destructuring_match)] + let src = match pixmap_src { + DynamicPixmapRef::U8(p) => p, + #[cfg(feature = "16bpc")] + DynamicPixmapRef::U16(_) => unreachable!(), + }; + let dst: &mut SubPixmapMut = unsafe { core::mem::transmute(pixmap_dst) }; highp::start( + functions.as_slice(), + tail_functions.as_slice(), + rect, + aa_mask_ctx, + mask_ctx, + &mut self.ctx, + src, + dst, + ); + } + #[cfg(feature = "16bpc")] + RasterPipelineKindGeneric::HighU16 { + ref functions, + ref tail_functions, + } => { + let dst: &mut crate::SubPixmapU16Mut = unsafe { core::mem::transmute(pixmap_dst) }; + highp::start_u16( functions.as_slice(), tail_functions.as_slice(), rect, @@ -523,24 +583,24 @@ impl RasterPipeline { mask_ctx, &mut self.ctx, pixmap_src, - pixmap_dst, + dst, ); } - RasterPipelineKind::Low { + RasterPipelineKindGeneric::Low { ref functions, ref tail_functions, } => { - lowp::start( + P::run_lowp( functions.as_slice(), tail_functions.as_slice(), rect, aa_mask_ctx, mask_ctx, &mut self.ctx, - // lowp doesn't support pattern, so no `pixmap_src` for it. pixmap_dst, ); } + RasterPipelineKindGeneric::_Marker(_) => unreachable!(), } } } @@ -567,7 +627,7 @@ mod blend_tests { let mut pixmap = Pixmap::new(1, 1).unwrap(); pixmap.fill(Color::from_rgba8(50, 127, 150, 200)); - let pixmap_src = PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap(); + let pixmap_src = DynamicPixmapRef::dummy(); let mut p = RasterPipelineBuilder::new(); p.set_force_hq_pipeline($is_highp); diff --git a/src/pixmap.rs b/src/pixmap.rs index e576ef2..82bce29 100644 --- a/src/pixmap.rs +++ b/src/pixmap.rs @@ -14,64 +14,255 @@ use tiny_skia_path::IntSize; use crate::{Color, IntRect}; -use crate::color::PremultipliedColorU8; +#[cfg(feature = "16bpc")] +use crate::color::{ColorU16, PremultipliedColorU16, ALPHA_U16_OPAQUE, ALPHA_U16_TRANSPARENT}; +use crate::color::{ColorU8, PremultipliedColor, PremultipliedColorU8}; +use crate::color::{ALPHA_U8_OPAQUE, ALPHA_U8_TRANSPARENT}; use crate::geom::{IntSizeExt, ScreenIntRect}; #[cfg(feature = "png-format")] -use crate::color::{premultiply_u8, ALPHA_U8_OPAQUE}; +use crate::color::premultiply_u8; -/// Number of bytes per pixel. +/// Number of bytes per pixel for 8bpc. pub const BYTES_PER_PIXEL: usize = 4; +/// Sealed trait representing a pixel format with premultiplied color channels. +pub trait Pixel: + Copy + Clone + PartialEq + Eq + bytemuck::Pod + bytemuck::Zeroable + 'static +{ + /// Demultiplied color type. + type Color: Copy + Clone + PartialEq + Eq + core::fmt::Debug; + /// Alpha channel type. + type Alpha: Copy + Clone + Into; + /// Bytes per pixel. + const BYTES_PER_PIXEL: usize; + /// Fully opaque alpha value. + const ALPHA_OPAQUE: Self::Alpha; + /// Fully transparent alpha value. + const ALPHA_TRANSPARENT: Self::Alpha; + + /// Converts an RGBA float color into this premultiplied pixel type. + fn from_color(c: Color) -> Self; + /// Converts a floating-point premultiplied color into this pixel type. + fn from_premultiplied(c: PremultipliedColor) -> Self; + /// Returns demultiplied color. + fn demultiply(self) -> Self::Color; + /// Returns whether pixel is opaque. + fn is_opaque(self) -> bool; + /// Converts pixel to 8-bit premultiplied color. + fn to_u8(&self) -> PremultipliedColorU8; + /// Converts 8-bit premultiplied color to this pixel type. + fn from_u8(c: PremultipliedColorU8) -> Self; +} + +impl Pixel for PremultipliedColorU8 { + type Color = ColorU8; + type Alpha = u8; + const BYTES_PER_PIXEL: usize = 4; + const ALPHA_OPAQUE: u8 = ALPHA_U8_OPAQUE; + const ALPHA_TRANSPARENT: u8 = ALPHA_U8_TRANSPARENT; + + #[inline(always)] + fn from_color(c: Color) -> Self { + c.premultiply().to_color_u8() + } + + #[inline(always)] + fn from_premultiplied(c: PremultipliedColor) -> Self { + c.to_color_u8() + } + + #[inline(always)] + fn demultiply(self) -> Self::Color { + PremultipliedColorU8::demultiply(&self) + } + + #[inline(always)] + fn is_opaque(self) -> bool { + PremultipliedColorU8::is_opaque(&self) + } + + #[inline(always)] + fn to_u8(&self) -> PremultipliedColorU8 { + *self + } + + #[inline(always)] + fn from_u8(c: PremultipliedColorU8) -> Self { + c + } +} + +#[cfg(feature = "16bpc")] +impl Pixel for PremultipliedColorU16 { + type Color = ColorU16; + type Alpha = u16; + const BYTES_PER_PIXEL: usize = 8; + const ALPHA_OPAQUE: u16 = ALPHA_U16_OPAQUE; + const ALPHA_TRANSPARENT: u16 = ALPHA_U16_TRANSPARENT; + + #[inline(always)] + fn from_color(c: Color) -> Self { + c.premultiply().to_color_u16() + } + + #[inline(always)] + fn from_premultiplied(c: PremultipliedColor) -> Self { + c.to_color_u16() + } + + #[inline(always)] + fn demultiply(self) -> Self::Color { + PremultipliedColorU16::demultiply(&self) + } + + #[inline(always)] + fn is_opaque(self) -> bool { + PremultipliedColorU16::is_opaque(&self) + } + + #[inline(always)] + fn to_u8(&self) -> PremultipliedColorU8 { + self.to_color_u8() + } + + #[inline(always)] + fn from_u8(c: PremultipliedColorU8) -> Self { + PremultipliedColorU16::from_color_u8(c) + } +} + /// A container that owns premultiplied RGBA pixels. /// /// The data is not aligned, therefore width == stride. #[derive(Clone, PartialEq)] -pub struct Pixmap { +pub struct PixmapGeneric { data: Vec, size: IntSize, + _marker: core::marker::PhantomData

, } -impl Pixmap { +/// 8-bit per channel RGBA Pixmap. +pub type Pixmap = PixmapGeneric; + +/// 16-bit per channel RGBA Pixmap. +#[cfg(feature = "16bpc")] +pub type PixmapU16 = PixmapGeneric; + +impl PixmapGeneric

{ /// Allocates a new pixmap. /// /// A pixmap is filled with transparent black by default, aka (0, 0, 0, 0). /// /// Zero size in an error. - /// - /// Pixmap's width is limited by i32::MAX/4. pub fn new(width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; - - // We cannot check that allocation was successful yet. - // We have to wait for https://github.com/rust-lang/rust/issues/48043 + let data_len = data_len_for_size::

(size)?; - Some(Pixmap { + Some(PixmapGeneric { data: vec![0; data_len], size, + _marker: core::marker::PhantomData, }) } - /// Creates a new pixmap by taking ownership over an image buffer - /// (premultiplied RGBA pixels). - /// - /// The size needs to match the data provided. - /// - /// Pixmap's width is limited by i32::MAX/4. + /// Creates a new pixmap by taking ownership over an image buffer. pub fn from_vec(data: Vec, size: IntSize) -> Option { - let data_len = data_len_for_size(size)?; + let data_len = data_len_for_size::

(size)?; if data.len() != data_len { return None; } - Some(Pixmap { data, size }) + Some(PixmapGeneric { + data, + size, + _marker: core::marker::PhantomData, + }) } - /// Decodes a PNG data into a `Pixmap`. - /// - /// Only 8-bit images are supported. - /// Index PNGs are not supported. + /// Returns a container that references Pixmap's data. + pub fn as_ref(&self) -> PixmapRefGeneric<'_, P> { + PixmapRefGeneric { + data: &self.data, + size: self.size, + _marker: core::marker::PhantomData, + } + } + + /// Returns a container that references Pixmap's mutable data. + pub fn as_mut(&mut self) -> PixmapMutGeneric<'_, P> { + PixmapMutGeneric { + data: &mut self.data, + size: self.size, + _marker: core::marker::PhantomData, + } + } + + /// Returns pixmap's width. + #[inline] + pub fn width(&self) -> u32 { + self.size.width() + } + + /// Returns pixmap's height. + #[inline] + pub fn height(&self) -> u32 { + self.size.height() + } + + /// Returns pixmap's size. + #[inline] + pub fn size(&self) -> IntSize { + self.size + } + + /// Fills the entire pixmap with a specified color. + pub fn fill(&mut self, color: Color) { + let c = P::from_color(color); + for p in self.as_mut().pixels_mut() { + *p = c; + } + } + + /// Returns the internal data. + pub fn data(&self) -> &[u8] { + self.data.as_slice() + } + + /// Returns the mutable internal data. + pub fn data_mut(&mut self) -> &mut [u8] { + self.data.as_mut_slice() + } + + /// Returns a pixel color. + pub fn pixel(&self, x: u32, y: u32) -> Option

{ + let idx = self.width().checked_mul(y)?.checked_add(x)?; + self.pixels().get(idx as usize).cloned() + } + + /// Returns a mutable slice of pixels. + pub fn pixels_mut(&mut self) -> &mut [P] { + bytemuck::cast_slice_mut(self.data_mut()) + } + + /// Returns a slice of pixels. + pub fn pixels(&self) -> &[P] { + bytemuck::cast_slice(self.data()) + } + + /// Consumes the internal data. + pub fn take(self) -> Vec { + self.data + } + + /// Returns a copy of the pixmap that intersects the `rect`. + pub fn clone_rect(&self, rect: IntRect) -> Option { + self.as_ref().clone_rect(rect) + } +} + +impl PixmapGeneric { + /// Decodes a PNG data into an 8-bit `Pixmap`. #[cfg(feature = "png-format")] pub fn decode_png(data: &[u8]) -> Result { fn make_custom_png_error(msg: &str) -> png::DecodingError { @@ -93,8 +284,8 @@ impl Pixmap { let size = IntSize::from_wh(info.width, info.height) .ok_or_else(|| make_custom_png_error("invalid image size"))?; - let data_len = - data_len_for_size(size).ok_or_else(|| make_custom_png_error("image is too big"))?; + let data_len = data_len_for_size::(size) + .ok_or_else(|| make_custom_png_error("image is too big"))?; img_data = match info.color_type { png::ColorType::Rgb => { @@ -105,7 +296,6 @@ impl Pixmap { rgba_data.push(rgb[2]); rgba_data.push(ALPHA_U8_OPAQUE); } - rgba_data } png::ColorType::Rgba => img_data, @@ -117,7 +307,6 @@ impl Pixmap { rgba_data.push(gray); rgba_data.push(ALPHA_U8_OPAQUE); } - rgba_data } png::ColorType::GrayscaleAlpha => { @@ -130,7 +319,6 @@ impl Pixmap { rgba_data.push(gray); rgba_data.push(alpha); } - rgba_data } png::ColorType::Indexed => { @@ -138,14 +326,6 @@ impl Pixmap { } }; - // Premultiply alpha. - // - // We cannon use RasterPipeline here, which is faster, - // because it produces slightly different results. - // Seems like Skia does the same. - // - // Also, in our tests unsafe version (no bound checking) - // had roughly the same performance. So we keep the safe one. for pixel in img_data.as_mut_slice().chunks_mut(BYTES_PER_PIXEL) { let a = pixel[3]; pixel[0] = premultiply_u8(pixel[0], a); @@ -158,14 +338,8 @@ impl Pixmap { } /// Loads a PNG file into a `Pixmap`. - /// - /// Only 8-bit images are supported. - /// Index PNGs are not supported. #[cfg(feature = "png-format")] pub fn load_png>(path: P) -> Result { - // `png::Decoder` is generic over input, which means that it will instance - // two copies: one for `&[]` and one for `File`. Which will simply bloat the code. - // Therefore we're using only one type for input. let data = std::fs::read(path)?; Self::decode_png(&data) } @@ -182,112 +356,163 @@ impl Pixmap { self.as_ref().save_png(path) } - /// Returns a container that references Pixmap's data. - pub fn as_ref(&self) -> PixmapRef<'_> { - PixmapRef { - data: &self.data, - size: self.size, + /// Consumes the pixmap and returns the internal data as demultiplied RGBA bytes. + pub fn take_demultiplied(mut self) -> Vec { + for pixel in self.pixels_mut() { + let c = pixel.demultiply(); + *pixel = + PremultipliedColorU8::from_rgba_unchecked(c.red(), c.green(), c.blue(), c.alpha()); } + self.data } +} - /// Returns a container that references Pixmap's data. - pub fn as_mut(&mut self) -> PixmapMut<'_> { - PixmapMut { - data: &mut self.data, - size: self.size, +#[cfg(feature = "16bpc")] +impl PixmapGeneric { + /// Consumes the pixmap and returns the internal data as demultiplied RGBA64 bytes. + pub fn take_demultiplied(mut self) -> Vec { + for pixel in self.pixels_mut() { + let c = pixel.demultiply(); + *pixel = + PremultipliedColorU16::from_rgba_unchecked(c.red(), c.green(), c.blue(), c.alpha()); } + self.data } - /// Returns pixmap's width. - #[inline] - pub fn width(&self) -> u32 { - self.size.width() - } - - /// Returns pixmap's height. - #[inline] - pub fn height(&self) -> u32 { - self.size.height() - } - - /// Returns pixmap's size. - #[allow(dead_code)] - pub(crate) fn size(&self) -> IntSize { - self.size - } - - /// Fills the entire pixmap with a specified color. - pub fn fill(&mut self, color: Color) { - let c = color.premultiply().to_color_u8(); - for p in self.as_mut().pixels_mut() { - *p = c; + /// Encodes pixmap into a 16-bit PNG data. + #[cfg(feature = "png-format")] + pub fn encode_png(&self) -> Result, png::EncodingError> { + let demultiplied_data = self.clone().take_demultiplied(); + let mut data = Vec::new(); + { + let mut encoder = png::Encoder::new(&mut data, self.width(), self.height()); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Sixteen); + let mut writer = encoder.write_header()?; + let be_bytes: Vec = demultiplied_data + .chunks_exact(2) + .flat_map(|chunk| { + let val = u16::from_ne_bytes([chunk[0], chunk[1]]); + val.to_be_bytes() + }) + .collect(); + writer.write_image_data(&be_bytes)?; } + Ok(data) } - /// Returns the internal data. - /// - /// Byteorder: RGBA - pub fn data(&self) -> &[u8] { - self.data.as_slice() - } - - /// Returns the mutable internal data. - /// - /// Byteorder: RGBA - pub fn data_mut(&mut self) -> &mut [u8] { - self.data.as_mut_slice() - } - - /// Returns a pixel color. - /// - /// Returns `None` when position is out of bounds. - pub fn pixel(&self, x: u32, y: u32) -> Option { - let idx = self.width().checked_mul(y)?.checked_add(x)?; - self.pixels().get(idx as usize).cloned() - } - - /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { - bytemuck::cast_slice_mut(self.data_mut()) + /// Saves pixmap as a 16-bit PNG file. + #[cfg(feature = "png-format")] + pub fn save_png>( + &self, + path: Pth, + ) -> Result<(), png::EncodingError> { + let data = self.encode_png()?; + std::fs::write(path, data)?; + Ok(()) } - /// Returns a slice of pixels. - pub fn pixels(&self) -> &[PremultipliedColorU8] { - bytemuck::cast_slice(self.data()) - } + /// Decodes a PNG image into a `PixmapU16`. + #[cfg(feature = "png-format")] + pub fn decode_png(data: &[u8]) -> Result { + fn make_custom_png_error(msg: &str) -> png::DecodingError { + std::io::Error::new(std::io::ErrorKind::Other, msg).into() + } - /// Consumes the internal data. - /// - /// Byteorder: RGBA - pub fn take(self) -> Vec { - self.data - } + let decoder = png::Decoder::new(std::io::BufReader::new(std::io::Cursor::new(data))); + let mut reader = decoder.read_info()?; + let output_buffer_size = reader + .output_buffer_size() + .ok_or(png::DecodingError::LimitsExceeded)?; + let mut img_data = vec![0; output_buffer_size]; + let info = reader.next_frame(&mut img_data)?; - /// Consumes the pixmap and returns the internal data as demultiplied RGBA bytes. - /// - /// Byteorder: RGBA - pub fn take_demultiplied(mut self) -> Vec { - // Demultiply alpha. - // - // RasterPipeline is 15% faster here, but produces slightly different results - // due to rounding. So we stick with this method for now. - for pixel in self.pixels_mut() { - let c = pixel.demultiply(); - *pixel = - PremultipliedColorU8::from_rgba_unchecked(c.red(), c.green(), c.blue(), c.alpha()); + let size = IntSize::from_wh(info.width, info.height) + .ok_or_else(|| make_custom_png_error("invalid image size"))?; + let num_pixels = (info.width as usize) * (info.height as usize); + let mut pixels = Vec::with_capacity(num_pixels); + + match (info.bit_depth, info.color_type) { + (png::BitDepth::Sixteen, png::ColorType::Rgba) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(8) { + let r = u16::from_be_bytes([chunk[0], chunk[1]]); + let g = u16::from_be_bytes([chunk[2], chunk[3]]); + let b = u16::from_be_bytes([chunk[4], chunk[5]]); + let a = u16::from_be_bytes([chunk[6], chunk[7]]); + let c = ColorU16::from_rgba(r, g, b, a); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Sixteen, png::ColorType::Rgb) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(6) { + let r = u16::from_be_bytes([chunk[0], chunk[1]]); + let g = u16::from_be_bytes([chunk[2], chunk[3]]); + let b = u16::from_be_bytes([chunk[4], chunk[5]]); + let c = ColorU16::from_rgba(r, g, b, 65535); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Sixteen, png::ColorType::Grayscale) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(2) { + let gray = u16::from_be_bytes([chunk[0], chunk[1]]); + let c = ColorU16::from_rgba(gray, gray, gray, 65535); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Sixteen, png::ColorType::GrayscaleAlpha) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(4) { + let gray = u16::from_be_bytes([chunk[0], chunk[1]]); + let alpha = u16::from_be_bytes([chunk[2], chunk[3]]); + let c = ColorU16::from_rgba(gray, gray, gray, alpha); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Eight, png::ColorType::Rgba) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(4) { + let r = ((chunk[0] as u16) << 8) | (chunk[0] as u16); + let g = ((chunk[1] as u16) << 8) | (chunk[1] as u16); + let b = ((chunk[2] as u16) << 8) | (chunk[2] as u16); + let a = ((chunk[3] as u16) << 8) | (chunk[3] as u16); + let c = ColorU16::from_rgba(r, g, b, a); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Eight, png::ColorType::Rgb) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(3) { + let r = ((chunk[0] as u16) << 8) | (chunk[0] as u16); + let g = ((chunk[1] as u16) << 8) | (chunk[1] as u16); + let b = ((chunk[2] as u16) << 8) | (chunk[2] as u16); + let c = ColorU16::from_rgba(r, g, b, 65535); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Eight, png::ColorType::Grayscale) => { + for &gray8 in &img_data[..info.buffer_size()] { + let gray = ((gray8 as u16) << 8) | (gray8 as u16); + let c = ColorU16::from_rgba(gray, gray, gray, 65535); + pixels.push(c.premultiply()); + } + } + (png::BitDepth::Eight, png::ColorType::GrayscaleAlpha) => { + for chunk in img_data[..info.buffer_size()].chunks_exact(2) { + let gray = ((chunk[0] as u16) << 8) | (chunk[0] as u16); + let alpha = ((chunk[1] as u16) << 8) | (chunk[1] as u16); + let c = ColorU16::from_rgba(gray, gray, gray, alpha); + pixels.push(c.premultiply()); + } + } + _ => { + return Err(make_custom_png_error("unsupported PNG format")); + } } - self.data - } - /// Returns a copy of the pixmap that intersects the `rect`. - /// - /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. - pub fn clone_rect(&self, rect: IntRect) -> Option { - self.as_ref().clone_rect(rect) + let raw_bytes: Vec = bytemuck::cast_slice(&pixels).to_vec(); + PixmapGeneric::from_vec(raw_bytes, size) + .ok_or_else(|| make_custom_png_error("failed to create pixmap")) } } -impl core::fmt::Debug for Pixmap { +impl core::fmt::Debug for PixmapGeneric

{ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Pixmap") .field("data", &"...") @@ -298,40 +523,42 @@ impl core::fmt::Debug for Pixmap { } /// A container that references premultiplied RGBA pixels. -/// -/// Can be created from `Pixmap` or from a user provided data. -/// -/// The data is not aligned, therefore width == stride. #[derive(Clone, Copy, PartialEq)] -pub struct PixmapRef<'a> { +pub struct PixmapRefGeneric<'a, P: Pixel> { data: &'a [u8], size: IntSize, + _marker: core::marker::PhantomData

, } -impl<'a> PixmapRef<'a> { +/// 8-bit per channel Pixmap reference. +pub type PixmapRef<'a> = PixmapRefGeneric<'a, PremultipliedColorU8>; + +/// 16-bit per channel Pixmap reference. +#[cfg(feature = "16bpc")] +pub type PixmapU16Ref<'a> = PixmapRefGeneric<'a, PremultipliedColorU16>; + +impl<'a, P: Pixel> PixmapRefGeneric<'a, P> { /// Creates a new `PixmapRef` from bytes. - /// - /// The size must be at least `size.width() * size.height() * BYTES_PER_PIXEL`. - /// Zero size in an error. Width is limited by i32::MAX/4. - /// - /// The `data` is assumed to have premultiplied RGBA pixels (byteorder: RGBA). pub fn from_bytes(data: &'a [u8], width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; + let data_len = data_len_for_size::

(size)?; if data.len() < data_len { return None; } - Some(PixmapRef { data, size }) + Some(PixmapRefGeneric { + data, + size, + _marker: core::marker::PhantomData, + }) } /// Creates a new `Pixmap` from the current data. - /// - /// Clones the underlying data. - pub fn to_owned(&self) -> Pixmap { - Pixmap { + pub fn to_owned(&self) -> PixmapGeneric

{ + PixmapGeneric { data: self.data.to_vec(), size: self.size, + _marker: core::marker::PhantomData, } } @@ -358,41 +585,30 @@ impl<'a> PixmapRef<'a> { } /// Returns the internal data. - /// - /// Byteorder: RGBA pub fn data(&self) -> &'a [u8] { self.data } /// Returns a pixel color. - /// - /// Returns `None` when position is out of bounds. - pub fn pixel(&self, x: u32, y: u32) -> Option { + pub fn pixel(&self, x: u32, y: u32) -> Option

{ let idx = self.width().checked_mul(y)?.checked_add(x)?; self.pixels().get(idx as usize).cloned() } /// Returns a slice of pixels. - pub fn pixels(&self) -> &'a [PremultipliedColorU8] { + pub fn pixels(&self) -> &'a [P] { bytemuck::cast_slice(self.data()) } - // TODO: add rows() iterator - /// Returns a copy of the pixmap that intersects the `rect`. - /// - /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. - pub fn clone_rect(&self, rect: IntRect) -> Option { - // TODO: to ScreenIntRect? - + pub fn clone_rect(&self, rect: IntRect) -> Option> { let rect = self.rect().to_int_rect().intersect(&rect)?; - let mut new = Pixmap::new(rect.width(), rect.height())?; + let mut new = PixmapGeneric::

::new(rect.width(), rect.height())?; { let old_pixels = self.pixels(); let mut new_mut = new.as_mut(); let new_pixels = new_mut.pixels_mut(); - // TODO: optimize for y in 0..rect.height() { for x in 0..rect.width() { let old_idx = (y + rect.y() as u32) * self.width() + (x + rect.x() as u32); @@ -404,17 +620,13 @@ impl<'a> PixmapRef<'a> { Some(new) } +} +impl PixmapRefGeneric<'_, PremultipliedColorU8> { /// Encodes pixmap into a PNG data. #[cfg(feature = "png-format")] pub fn encode_png(&self) -> Result, png::EncodingError> { - // Skia uses skcms here, which is somewhat similar to RasterPipeline. - - // Sadly, we have to copy the pixmap here, because of demultiplication. - // Not sure how to avoid this. - // TODO: remove allocation let demultiplied_data = self.to_owned().take_demultiplied(); - let mut data = Vec::new(); { let mut encoder = png::Encoder::new(&mut data, self.width(), self.height()); @@ -423,7 +635,6 @@ impl<'a> PixmapRef<'a> { let mut writer = encoder.write_header()?; writer.write_image_data(&demultiplied_data)?; } - Ok(data) } @@ -436,7 +647,7 @@ impl<'a> PixmapRef<'a> { } } -impl core::fmt::Debug for PixmapRef<'_> { +impl core::fmt::Debug for PixmapRefGeneric<'_, P> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("PixmapRef") .field("data", &"...") @@ -447,48 +658,51 @@ impl core::fmt::Debug for PixmapRef<'_> { } /// A container that references mutable premultiplied RGBA pixels. -/// -/// Can be created from `Pixmap` or from a user provided data. -/// -/// The data is not aligned, therefore width == stride. #[derive(PartialEq)] -pub struct PixmapMut<'a> { +pub struct PixmapMutGeneric<'a, P: Pixel> { data: &'a mut [u8], size: IntSize, + _marker: core::marker::PhantomData

, } -impl<'a> PixmapMut<'a> { +/// 8-bit per channel Pixmap mutable reference. +pub type PixmapMut<'a> = PixmapMutGeneric<'a, PremultipliedColorU8>; + +/// 16-bit per channel Pixmap mutable reference. +#[cfg(feature = "16bpc")] +pub type PixmapU16Mut<'a> = PixmapMutGeneric<'a, PremultipliedColorU16>; + +impl<'a, P: Pixel> PixmapMutGeneric<'a, P> { /// Creates a new `PixmapMut` from bytes. - /// - /// The size must be at least `size.width() * size.height() * BYTES_PER_PIXEL`. - /// Zero size in an error. Width is limited by i32::MAX/4. - /// - /// The `data` is assumed to have premultiplied RGBA pixels (byteorder: RGBA). pub fn from_bytes(data: &'a mut [u8], width: u32, height: u32) -> Option { let size = IntSize::from_wh(width, height)?; - let data_len = data_len_for_size(size)?; + let data_len = data_len_for_size::

(size)?; if data.len() < data_len { return None; } - Some(PixmapMut { data, size }) + Some(PixmapMutGeneric { + data, + size, + _marker: core::marker::PhantomData, + }) } /// Creates a new `Pixmap` from the current data. - /// - /// Clones the underlying data. - pub fn to_owned(&self) -> Pixmap { - Pixmap { + pub fn to_owned(&self) -> PixmapGeneric

{ + PixmapGeneric { data: self.data.to_vec(), size: self.size, + _marker: core::marker::PhantomData, } } /// Returns a container that references Pixmap's data. - pub fn as_ref(&self) -> PixmapRef<'_> { - PixmapRef { + pub fn as_ref(&self) -> PixmapRefGeneric<'_, P> { + PixmapRefGeneric { data: self.data, size: self.size, + _marker: core::marker::PhantomData, } } @@ -511,50 +725,49 @@ impl<'a> PixmapMut<'a> { /// Fills the entire pixmap with a specified color. pub fn fill(&mut self, color: Color) { - let c = color.premultiply().to_color_u8(); + let c = P::from_color(color); for p in self.pixels_mut() { *p = c; } } /// Returns the mutable internal data. - /// - /// Byteorder: RGBA pub fn data_mut(&mut self) -> &mut [u8] { self.data } /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { + pub fn pixels_mut(&mut self) -> &mut [P] { bytemuck::cast_slice_mut(self.data_mut()) } /// Creates `SubPixmapMut` that contains the whole `PixmapMut`. - pub(crate) fn as_subpixmap(&mut self) -> SubPixmapMut<'_> { - SubPixmapMut { + pub(crate) fn as_subpixmap(&mut self) -> SubPixmapMutGeneric<'_, P> { + let width = self.width() as usize; + SubPixmapMutGeneric { size: self.size(), - real_width: self.width() as usize, + real_width: width, data: self.data, + _marker: core::marker::PhantomData, } } /// Returns a mutable reference to the pixmap region that intersects the `rect`. - /// - /// Returns `None` when `Pixmap`'s rect doesn't contain `rect`. - pub(crate) fn subpixmap(&mut self, rect: IntRect) -> Option> { + pub(crate) fn subpixmap(&mut self, rect: IntRect) -> Option> { let rect = self.size.to_int_rect(0, 0).intersect(&rect)?; - let row_bytes = self.width() as usize * BYTES_PER_PIXEL; - let offset = rect.top() as usize * row_bytes + rect.left() as usize * BYTES_PER_PIXEL; + let row_bytes = self.width() as usize * P::BYTES_PER_PIXEL; + let offset = rect.top() as usize * row_bytes + rect.left() as usize * P::BYTES_PER_PIXEL; - Some(SubPixmapMut { + Some(SubPixmapMutGeneric { size: rect.size(), real_width: self.width() as usize, data: &mut self.data[offset..], + _marker: core::marker::PhantomData, }) } } -impl core::fmt::Debug for PixmapMut<'_> { +impl core::fmt::Debug for PixmapMutGeneric<'_, P> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("PixmapMut") .field("data", &"...") @@ -565,48 +778,179 @@ impl core::fmt::Debug for PixmapMut<'_> { } /// A `PixmapMut` subregion. -/// -/// Unlike `PixmapMut`, contains `real_width` which references the parent `PixmapMut` width. -/// This way we can operate on a `PixmapMut` subregion without reallocations. -/// Primarily required because of `DrawTiler`. -/// -/// We cannot implement it in `PixmapMut` directly, because it will brake `fill`, `data_mut` -/// `pixels_mut` and other similar methods. -/// This is because `SubPixmapMut.data` references more "data" than it actually allowed to access. -/// On the other hand, `PixmapMut.data` can access all it's data and it's stored linearly. -pub struct SubPixmapMut<'a> { +pub struct SubPixmapMutGeneric<'a, P: Pixel> { + /// Pixmap data. pub data: &'a mut [u8], + /// Region size. pub size: IntSize, + /// Width of the parent Pixmap. pub real_width: usize, + /// Pixel type marker. + pub _marker: core::marker::PhantomData

, +} + +impl core::fmt::Debug for SubPixmapMutGeneric<'_, P> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SubPixmapMut") + .field("data", &"...") + .field("width", &self.size.width()) + .field("height", &self.size.height()) + .finish() + } } -impl SubPixmapMut<'_> { +/// 8-bit per channel SubPixmap mutable reference. +pub type SubPixmapMut<'a> = SubPixmapMutGeneric<'a, PremultipliedColorU8>; + +/// 16-bit per channel SubPixmap mutable reference. +#[cfg(feature = "16bpc")] +pub type SubPixmapU16Mut<'a> = SubPixmapMutGeneric<'a, PremultipliedColorU16>; + +impl<'a, P: Pixel> SubPixmapMutGeneric<'a, P> { /// Returns a mutable slice of pixels. - pub fn pixels_mut(&mut self) -> &mut [PremultipliedColorU8] { + pub fn pixels_mut(&mut self) -> &mut [P] { bytemuck::cast_slice_mut(self.data) } } /// Returns minimum bytes per row as usize. -/// -/// Pixmap's maximum value for row bytes must fit in 31 bits. -fn min_row_bytes(size: IntSize) -> Option { +fn min_row_bytes(size: IntSize) -> Option { let w = i32::try_from(size.width()).ok()?; - let w = w.checked_mul(BYTES_PER_PIXEL as i32)?; + let w = w.checked_mul(P::BYTES_PER_PIXEL as i32)?; NonZeroUsize::new(w as usize) } /// Returns storage size required by pixel array. -fn compute_data_len(size: IntSize, row_bytes: usize) -> Option { +fn compute_data_len(size: IntSize, row_bytes: usize) -> Option { let h = size.height().checked_sub(1)?; let h = (h as usize).checked_mul(row_bytes)?; + let w = (size.width() as usize).checked_mul(P::BYTES_PER_PIXEL)?; + h.checked_add(w) +} - let w = (size.width() as usize).checked_mul(BYTES_PER_PIXEL)?; +fn data_len_for_size(size: IntSize) -> Option { + let row_bytes = min_row_bytes::

(size)?; + compute_data_len::

(size, row_bytes.get()) +} - h.checked_add(w) +/// A dynamically-typed pixmap holding either 8bpc (RGBA8) or 16bpc (RGBA16) pixel data. +#[cfg(feature = "16bpc")] +#[derive(Clone, PartialEq, Debug)] +pub enum DynamicPixmap { + /// 8-bit per channel pixmap. + U8(Pixmap), + /// 16-bit per channel pixmap. + U16(PixmapU16), } -fn data_len_for_size(size: IntSize) -> Option { - let row_bytes = min_row_bytes(size)?; - compute_data_len(size, row_bytes.get()) +#[cfg(feature = "16bpc")] +impl DynamicPixmap { + /// Returns the width of the pixmap. + pub fn width(&self) -> u32 { + match self { + DynamicPixmap::U8(p) => p.width(), + DynamicPixmap::U16(p) => p.width(), + } + } + + /// Returns the height of the pixmap. + pub fn height(&self) -> u32 { + match self { + DynamicPixmap::U8(p) => p.height(), + DynamicPixmap::U16(p) => p.height(), + } + } + + /// Returns a dynamic mutable reference. + pub fn as_mut(&mut self) -> DynamicPixmapMut<'_> { + match self { + DynamicPixmap::U8(p) => DynamicPixmapMut::U8(p.as_mut()), + DynamicPixmap::U16(p) => DynamicPixmapMut::U16(p.as_mut()), + } + } + + /// Encodes pixmap into PNG data. + #[cfg(feature = "png-format")] + pub fn encode_png(&self) -> Result, png::EncodingError> { + match self { + DynamicPixmap::U8(p) => p.encode_png(), + DynamicPixmap::U16(p) => p.encode_png(), + } + } + + /// Saves pixmap as a PNG file. + #[cfg(feature = "png-format")] + pub fn save_png>( + &self, + path: Pth, + ) -> Result<(), png::EncodingError> { + match self { + DynamicPixmap::U8(p) => p.save_png(path), + DynamicPixmap::U16(p) => p.save_png(path), + } + } +} + +/// A dynamically-typed mutable pixmap reference. +#[cfg(feature = "16bpc")] +#[derive(PartialEq, Debug)] +pub enum DynamicPixmapMut<'a> { + /// 8-bit per channel pixmap mutable reference. + U8(PixmapMut<'a>), + /// 16-bit per channel pixmap mutable reference. + U16(PixmapU16Mut<'a>), +} + +#[cfg(feature = "16bpc")] +impl DynamicPixmapMut<'_> { + /// Returns the width of the pixmap. + pub fn width(&self) -> u32 { + match self { + DynamicPixmapMut::U8(p) => p.width(), + DynamicPixmapMut::U16(p) => p.width(), + } + } + + /// Returns the height of the pixmap. + pub fn height(&self) -> u32 { + match self { + DynamicPixmapMut::U8(p) => p.height(), + DynamicPixmapMut::U16(p) => p.height(), + } + } +} + +/// A dynamically-typed immutable pixmap reference. +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum DynamicPixmapRef<'a> { + /// 8-bit per channel pixmap reference. + U8(PixmapRef<'a>), + /// 16-bit per channel pixmap reference. + #[cfg(feature = "16bpc")] + U16(PixmapU16Ref<'a>), +} + +impl<'a> DynamicPixmapRef<'a> { + /// Creates a dummy 1x1 8bpc pixmap reference. + pub fn dummy() -> Self { + DynamicPixmapRef::U8(PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap()) + } + + /// Returns the width of the pixmap. + pub fn width(&self) -> u32 { + match self { + DynamicPixmapRef::U8(p) => p.width(), + #[cfg(feature = "16bpc")] + DynamicPixmapRef::U16(p) => p.width(), + } + } + + /// Returns the height of the pixmap. + pub fn height(&self) -> u32 { + match self { + DynamicPixmapRef::U8(p) => p.height(), + #[cfg(feature = "16bpc")] + DynamicPixmapRef::U16(p) => p.height(), + } + } } diff --git a/src/shaders/pattern.rs b/src/shaders/pattern.rs index c2a52d7..99b6305 100644 --- a/src/shaders/pattern.rs +++ b/src/shaders/pattern.rs @@ -6,7 +6,12 @@ use tiny_skia_path::NormalizedF32; -use crate::{BlendMode, ColorSpace, PixmapRef, Shader, SpreadMode, Transform}; +#[cfg(feature = "16bpc")] +use crate::PixmapU16Ref; +use crate::{ + BlendMode, ColorSpace, DynamicPixmapRef, Pixel, PixmapRef, PixmapRefGeneric, Shader, + SpreadMode, Transform, +}; use crate::pipeline; use crate::pipeline::RasterPipelineBuilder; @@ -36,12 +41,10 @@ pub struct PixmapPaint { /// /// Default: 1.0 pub opacity: f32, - /// Pixmap blending mode. /// /// Default: SourceOver pub blend_mode: BlendMode, - /// Specifies how much filtering to be done when transforming images. /// /// Default: Nearest @@ -66,7 +69,7 @@ impl Default for PixmapPaint { /// mipmap generation, which adds too much complexity. #[derive(Clone, PartialEq, Debug)] pub struct Pattern<'a> { - pub(crate) pixmap: PixmapRef<'a>, + pub(crate) pixmap: DynamicPixmapRef<'a>, quality: FilterQuality, spread_mode: SpreadMode, pub(crate) opacity: NormalizedF32, @@ -74,7 +77,7 @@ pub struct Pattern<'a> { } impl<'a> Pattern<'a> { - /// Creates a new pattern shader. + /// Creates a new pattern shader from an 8-bit pixmap. /// /// `opacity` will be clamped to the 0..=1 range. #[allow(clippy::new_ret_no_self)] @@ -86,7 +89,27 @@ impl<'a> Pattern<'a> { transform: Transform, ) -> Shader<'a> { Shader::Pattern(Pattern { - pixmap, + pixmap: DynamicPixmapRef::U8(pixmap), + spread_mode, + quality, + opacity: NormalizedF32::new_clamped(opacity), + transform, + }) + } + + /// Creates a new pattern shader from a 16-bit pixmap. + /// + /// `opacity` will be clamped to the 0..=1 range. + #[cfg(feature = "16bpc")] + pub fn new_u16( + pixmap: PixmapU16Ref<'a>, + spread_mode: SpreadMode, + quality: FilterQuality, + opacity: f32, + transform: Transform, + ) -> Shader<'a> { + Shader::Pattern(Pattern { + pixmap: DynamicPixmapRef::U16(pixmap), spread_mode, quality, opacity: NormalizedF32::new_clamped(opacity), @@ -94,6 +117,25 @@ impl<'a> Pattern<'a> { }) } + /// Creates a new pattern shader from a generic pixmap reference. + pub fn from_pixmap( + pixmap: PixmapRefGeneric<'a, P>, + spread_mode: SpreadMode, + quality: FilterQuality, + opacity: f32, + transform: Transform, + ) -> Shader<'a> { + #[cfg(feature = "16bpc")] + if P::BYTES_PER_PIXEL == 8 { + let p16 = + PixmapU16Ref::from_bytes(pixmap.data(), pixmap.width(), pixmap.height()).unwrap(); + return Self::new_u16(p16, spread_mode, quality, opacity, transform); + } + + let p8 = PixmapRef::from_bytes(pixmap.data(), pixmap.width(), pixmap.height()).unwrap(); + Self::new(p8, spread_mode, quality, opacity, transform) + } + pub(crate) fn push_stages(&self, cs: ColorSpace, p: &mut RasterPipelineBuilder) -> bool { let ts = match self.transform.invert() { Some(v) => v, diff --git a/tests/integration/common.rs b/tests/integration/common.rs new file mode 100644 index 0000000..8d661fc --- /dev/null +++ b/tests/integration/common.rs @@ -0,0 +1,78 @@ +use tiny_skia::*; + +#[track_caller] +pub fn assert_pixmap_eq(actual: &PixmapGeneric

, expected: &Pixmap, tolerance: u8) { + assert_eq!(actual.width(), expected.width(), "pixmap width mismatch"); + assert_eq!(actual.height(), expected.height(), "pixmap height mismatch"); + + for (i, (act, exp)) in actual + .pixels() + .iter() + .zip(expected.pixels().iter()) + .enumerate() + { + let act_u8 = act.to_u8(); + let dr = (act_u8.red() as i32 - exp.red() as i32).abs(); + let dg = (act_u8.green() as i32 - exp.green() as i32).abs(); + let db = (act_u8.blue() as i32 - exp.blue() as i32).abs(); + let da = (act_u8.alpha() as i32 - exp.alpha() as i32).abs(); + let max_diff = dr.max(dg).max(db).max(da); + if max_diff > tolerance as i32 { + let x = (i as u32) % actual.width(); + let y = (i as u32) / actual.width(); + panic!( + "Pixel ({}, {}) mismatch (tolerance {}): actual RGBA({}, {}, {}, {}), expected RGBA({}, {}, {}, {}), max diff {}", + x, y, tolerance, + act_u8.red(), act_u8.green(), act_u8.blue(), act_u8.alpha(), + exp.red(), exp.green(), exp.blue(), exp.alpha(), + max_diff + ); + } + } +} + +#[track_caller] +pub fn assert_mask_eq(actual: &Mask, expected: &Mask, tolerance: u8) { + assert_eq!(actual.width(), expected.width(), "mask width mismatch"); + assert_eq!(actual.height(), expected.height(), "mask height mismatch"); + + for (i, (act, exp)) in actual.data().iter().zip(expected.data().iter()).enumerate() { + let diff = (*act as i32 - *exp as i32).abs(); + if diff > tolerance as i32 { + let x = (i as u32) % actual.width(); + let y = (i as u32) / actual.width(); + panic!( + "Mask pixel ({}, {}) mismatch (tolerance {}): actual {}, expected {}, diff {}", + x, y, tolerance, act, exp, diff + ); + } + } +} + +#[macro_export] +macro_rules! test_raster { + ($name:ident, $w:expr, $h:expr, $expected_png:expr, tolerance: $tol:expr, |$pixmap:ident| $body:block) => { + #[test] + fn $name() { + let expected = tiny_skia::Pixmap::load_png($expected_png).unwrap(); + + // 8bpc run (strict byte-for-byte matching) + { + let mut $pixmap = tiny_skia::Pixmap::new($w, $h).unwrap(); + $body; + $crate::common::assert_pixmap_eq(&$pixmap, &expected, 0); + } + + // 16bpc run (tolerance for subpixel/gamma rounding) + #[cfg(feature = "16bpc")] + { + let mut $pixmap = tiny_skia::PixmapU16::new($w, $h).unwrap(); + $body; + $crate::common::assert_pixmap_eq(&$pixmap, &expected, $tol); + } + } + }; + ($name:ident, $w:expr, $h:expr, $expected_png:expr, |$pixmap:ident| $body:block) => { + test_raster!($name, $w, $h, $expected_png, tolerance: 2, |$pixmap| $body); + }; +} diff --git a/tests/integration/dash.rs b/tests/integration/dash.rs index 51cc31f..7fc0986 100644 --- a/tests/integration/dash.rs +++ b/tests/integration/dash.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn line() { +test_raster!(line, 100, 100, "tests/images/dash/line.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(10.0, 20.0); pb.line_to(90.0, 80.0); @@ -15,15 +14,10 @@ fn line() { stroke.dash = StrokeDash::new(vec![5.0, 10.0], 0.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/line.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn quad() { +test_raster!(quad, 100, 100, "tests/images/dash/quad.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(10.0, 20.0); pb.quad_to(35.0, 75.0, 90.0, 80.0); @@ -37,15 +31,10 @@ fn quad() { stroke.dash = StrokeDash::new(vec![5.0, 10.0], 0.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/quad.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn cubic() { +test_raster!(cubic, 100, 100, "tests/images/dash/cubic.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(10.0, 20.0); pb.cubic_to(95.0, 35.0, 0.0, 75.0, 75.0, 90.0); @@ -59,15 +48,10 @@ fn cubic() { stroke.dash = StrokeDash::new(vec![5.0, 10.0], 0.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/cubic.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn hairline() { +test_raster!(hairline, 100, 100, "tests/images/dash/hairline.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(10.0, 20.0); pb.cubic_to(95.0, 35.0, 0.0, 75.0, 75.0, 90.0); @@ -81,15 +65,10 @@ fn hairline() { stroke.dash = StrokeDash::new(vec![5.0, 10.0], 0.0); stroke.width = 0.5; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/hairline.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn complex() { +test_raster!(complex, 200, 200, "tests/images/dash/complex.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(28.7, 23.9); pb.line_to(177.4, 35.2); @@ -109,15 +88,10 @@ fn complex() { stroke.dash = StrokeDash::new(vec![10.0, 5.0], 2.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/complex.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn multi_subpaths() { +test_raster!(multi_subpaths, 200, 200, "tests/images/dash/multi_subpaths.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(49.0, 76.0); pb.cubic_to(22.0, 150.0, 11.0, 213.0, 186.0, 151.0); @@ -138,15 +112,10 @@ fn multi_subpaths() { stroke.dash = StrokeDash::new(vec![10.0, 5.0], 2.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/dash/multi_subpaths.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn closed() { +test_raster!(closed, 100, 100, "tests/images/dash/closed.png", |pixmap| { let mut pb = PathBuilder::new(); pb.move_to(22.0, 22.0); pb.cubic_to(63.0, 16.0, 82.0, 24.0, 84.0, 46.0); @@ -162,9 +131,5 @@ fn closed() { stroke.dash = StrokeDash::new(vec![10.0, 5.0], 2.0); stroke.width = 2.0; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/dash/closed.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/fill.rs b/tests/integration/fill.rs index 4737f48..1a3b911 100644 --- a/tests/integration/fill.rs +++ b/tests/integration/fill.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn horizontal_line() { +test_raster!(horizontal_line, 100, 100, "tests/images/fill/empty.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -11,15 +10,10 @@ fn horizontal_line() { pb.line_to(90.0, 10.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/empty.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn vertical_line() { +test_raster!(vertical_line, 100, 100, "tests/images/fill/empty.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -29,15 +23,10 @@ fn vertical_line() { pb.line_to(10.0, 90.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/empty.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn single_line() { +test_raster!(single_line, 100, 100, "tests/images/fill/empty.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -47,75 +36,50 @@ fn single_line() { pb.line_to(90.0, 90.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/empty.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn int_rect() { +test_raster!(int_rect, 100, 100, "tests/images/fill/int-rect.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; let rect = Rect::from_xywh(10.0, 15.0, 80.0, 70.0).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/int-rect.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn float_rect() { +test_raster!(float_rect, 100, 100, "tests/images/fill/float-rect.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; let rect = Rect::from_xywh(10.3, 15.4, 80.5, 70.6).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/float-rect.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn int_rect_aa() { +test_raster!(int_rect_aa, 100, 100, "tests/images/fill/int-rect-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; let rect = Rect::from_xywh(10.0, 15.0, 80.0, 70.0).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/int-rect-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn float_rect_aa() { +test_raster!(float_rect_aa, 100, 100, "tests/images/fill/float-rect-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; let rect = Rect::from_xywh(10.3, 15.4, 80.5, 70.6).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/float-rect-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn float_rect_aa_highp() { +test_raster!(float_rect_aa_highp, 100, 100, "tests/images/fill/float-rect-aa-highp.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -123,12 +87,8 @@ fn float_rect_aa_highp() { let rect = Rect::from_xywh(10.3, 15.4, 80.5, 70.6).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/fill/float-rect-aa-highp.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); #[test] fn tiny_float_rect() { @@ -140,22 +100,33 @@ fn tiny_float_rect() { let mut pixmap = Pixmap::new(3, 3).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); - assert_eq!( - pixmap.pixels(), - &[ - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + let expected = [ + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(50, 127, 150, 200).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(50, 127, 150, 200).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ColorU8::from_rgba(0, 0, 0, 0).premultiply(), - ] - ); + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ColorU8::from_rgba(0, 0, 0, 0).premultiply(), + ]; + assert_eq!(pixmap.pixels(), &expected); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(3, 3).unwrap(); + pixmap16.fill_rect(rect, &paint, Transform::identity(), None); + let center = pixmap16.pixel(1, 1).unwrap().to_u8(); + let expected = ColorU8::from_rgba(50, 127, 150, 200).premultiply(); + let dr = (center.red() as i32 - expected.red() as i32).abs(); + let dg = (center.green() as i32 - expected.green() as i32).abs(); + let db = (center.blue() as i32 - expected.blue() as i32).abs(); + let da = (center.alpha() as i32 - expected.alpha() as i32).abs(); + assert!(dr <= 1 && dg <= 1 && db <= 1 && da <= 1); + } } #[test] @@ -185,6 +156,19 @@ fn tiny_float_rect_aa() { ColorU8::from_rgba(0, 0, 0, 0).premultiply(), ] ); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(3, 3).unwrap(); + pixmap16.fill_rect(rect, &paint, Transform::identity(), None); + let center = pixmap16.pixel(1, 1).unwrap().to_u8(); + let expected = ColorU8::from_rgba(51, 128, 153, 60).premultiply(); + let dr = (center.red() as i32 - expected.red() as i32).abs(); + let dg = (center.green() as i32 - expected.green() as i32).abs(); + let db = (center.blue() as i32 - expected.blue() as i32).abs(); + let da = (center.alpha() as i32 - expected.alpha() as i32).abs(); + assert!(dr <= 2 && dg <= 2 && db <= 2 && da <= 2); + } } #[test] @@ -195,70 +179,55 @@ fn tiny_rect_aa() { let rect = Rect::from_xywh(0.7, 0.0, 1.0, 2.0).unwrap(); let mut pixmap = Pixmap::new(10, 10).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(10, 10).unwrap(); + pixmap16.fill_rect(rect, &paint, Transform::identity(), None); + } } -#[test] -fn float_rect_clip_top_left_aa() { +test_raster!(float_rect_clip_top_left_aa, 100, 100, "tests/images/fill/float-rect-clip-top-left-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; let rect = Rect::from_xywh(-10.3, -20.4, 100.5, 70.2).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/float-rect-clip-top-left-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn float_rect_clip_top_right_aa() { +test_raster!(float_rect_clip_top_right_aa, 100, 100, "tests/images/fill/float-rect-clip-top-right-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; let rect = Rect::from_xywh(60.3, -20.4, 100.5, 70.2).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/float-rect-clip-top-right-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn float_rect_clip_bottom_right_aa() { +test_raster!(float_rect_clip_bottom_right_aa, 100, 100, "tests/images/fill/float-rect-clip-bottom-right-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; let rect = Rect::from_xywh(60.3, 40.4, 100.5, 70.2).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/float-rect-clip-bottom-right-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn int_rect_with_ts_clip_right() { +test_raster!(int_rect_with_ts_clip_right, 100, 100, "tests/images/fill/int-rect-with-ts-clip-right.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect(rect, &paint, Transform::from_row(1.0, 0.0, 0.0, 1.0, 0.5, 0.5), None); +}); - let expected = Pixmap::load_png("tests/images/fill/int-rect-with-ts-clip-right.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn open_polygon() { +test_raster!(open_polygon, 100, 100, "tests/images/fill/polygon.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -271,16 +240,10 @@ fn open_polygon() { pb.line_to(90.744819, 40.864522); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/polygon.png").unwrap(); - assert_eq!(pixmap, expected); -} - -// Must be the same a open. -#[test] -fn closed_polygon() { +test_raster!(closed_polygon, 100, 100, "tests/images/fill/polygon.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -294,15 +257,10 @@ fn closed_polygon() { pb.close(); // the only difference let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/polygon.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn winding_star() { +test_raster!(winding_star, 100, 100, "tests/images/fill/winding-star.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -315,15 +273,10 @@ fn winding_star() { pb.line_to(25.0, 87.5); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/winding-star.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn even_odd_star() { +test_raster!(even_odd_star, 100, 100, "tests/images/fill/even-odd-star.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -336,15 +289,10 @@ fn even_odd_star() { pb.line_to(25.0, 87.5); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::EvenOdd, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/even-odd-star.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn quad_curve() { +test_raster!(quad_curve, 100, 100, "tests/images/fill/quad.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -354,15 +302,10 @@ fn quad_curve() { pb.quad_to(95.0, 35.0, 75.0, 90.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::EvenOdd, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/quad.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn cubic_curve() { +test_raster!(cubic_curve, 100, 100, "tests/images/fill/cubic.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -372,48 +315,30 @@ fn cubic_curve() { pb.cubic_to(95.0, 35.0, 0.0, 75.0, 75.0, 90.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::EvenOdd, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/cubic.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn memset2d() { +test_raster!(memset2d, 100, 100, "tests/images/fill/memset2d.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 255); // Must be opaque to trigger memset2d. paint.anti_alias = false; let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 90.0, 90.0).unwrap()); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/memset2d.png").unwrap(); - assert_eq!(pixmap, expected); -} - -// Make sure we do not write past pixmap memory. -#[test] -fn memset2d_out_of_bounds() { +test_raster!(memset2d_out_of_bounds, 100, 100, "tests/images/fill/memset2d-2.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 255); // Must be opaque to trigger memset2d. paint.anti_alias = false; let path = PathBuilder::from_rect(Rect::from_ltrb(50.0, 50.0, 120.0, 120.0).unwrap()); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/memset2d-2.png").unwrap(); - assert_eq!(pixmap, expected); -} - -// Not sure how to properly test anti-aliasing, -// so for now simply check that it actually applied. -#[test] -fn fill_aa() { +test_raster!(fill_aa, 100, 100, "tests/images/fill/star-aa.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -426,12 +351,8 @@ fn fill_aa() { pb.line_to(25.0, 87.5); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::EvenOdd, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/fill/star-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); #[test] fn overflow_in_walk_edges_1() { @@ -447,10 +368,15 @@ fn overflow_in_walk_edges_1() { // Must not panic. let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(100, 100).unwrap(); + pixmap16.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + } } -#[test] -fn clip_line_1() { +test_raster!(clip_line_1, 100, 100, "tests/images/fill/clip-line-1.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -463,15 +389,10 @@ fn clip_line_1() { pb.close(); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/clip-line-1.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn clip_line_2() { +test_raster!(clip_line_2, 100, 100, "tests/images/fill/clip-line-2.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -485,15 +406,10 @@ fn clip_line_2() { pb.close(); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/clip-line-2.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn clip_quad() { +test_raster!(clip_quad, 100, 100, "tests/images/fill/clip-quad.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -503,15 +419,10 @@ fn clip_quad() { pb.quad_to(150.0, 150.0, 85.0, 15.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/clip-quad.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn clip_cubic_1() { +test_raster!(clip_cubic_1, 100, 100, "tests/images/fill/clip-cubic-1.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -522,15 +433,10 @@ fn clip_cubic_1() { pb.cubic_to(0.0, 175.0, 195.0, 70.0, 75.0, 20.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/fill/clip-cubic-1.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn clip_cubic_2() { +test_raster!(clip_cubic_2, 100, 100, "tests/images/fill/clip-cubic-2.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -541,12 +447,8 @@ fn clip_cubic_2() { pb.cubic_to(10.0, 40.0, 90.0, 120.0, 125.0, 20.0); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/fill/clip-cubic-2.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); #[test] fn aa_endless_loop() { @@ -562,16 +464,20 @@ fn aa_endless_loop() { // Must not loop. let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(100, 100).unwrap(); + pixmap16.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + } } -#[test] -fn clear_aa() { +test_raster!(clear_aa, 100, 100, "tests/images/fill/clear-aa.png", |pixmap| { // Make sure that Clear with AA doesn't fallback to memset. let mut paint = Paint::default(); paint.anti_alias = true; paint.blend_mode = BlendMode::Clear; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill(Color::from_rgba8(50, 127, 150, 200)); pixmap.fill_path( &PathBuilder::from_circle(50.0, 50.0, 40.0).unwrap(), @@ -580,10 +486,7 @@ fn clear_aa() { Transform::identity(), None, ); - - let expected = Pixmap::load_png("tests/images/fill/clear-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); #[test] fn line_curve() { @@ -600,11 +503,14 @@ fn line_curve() { let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - // Must not panic. + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(200, 200).unwrap(); + pixmap16.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + } } -#[test] -fn vertical_lines_merging_bug() { +test_raster!(vertical_lines_merging_bug, 100, 100, "tests/images/fill/vertical-lines-merging-bug.png", |pixmap| { // This path must not trigger edge_builder::combine_vertical, // otherwise AlphaRuns::add will crash later. let mut pb = PathBuilder::new(); @@ -620,27 +526,19 @@ fn vertical_lines_merging_bug() { paint.anti_alias = true; // Must not panic. - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::from_row(5.4, 0.0, 0.0, 5.4, -4050.0, -840.0), None); +}); - let expected = Pixmap::load_png("tests/images/fill/vertical-lines-merging-bug.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn fill_rect() { +test_raster!(fill_rect, 100, 100, "tests/images/canvas/fill-rect.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect( Rect::from_xywh(20.3, 10.4, 50.5, 30.2).unwrap(), &paint, Transform::from_row(1.2, 0.3, -0.7, 0.8, 12.0, 15.3), None, ); +}); - let expected = Pixmap::load_png("tests/images/canvas/fill-rect.png").unwrap(); - assert_eq!(pixmap, expected); -} diff --git a/tests/integration/gamma.rs b/tests/integration/gamma.rs index 7c2b257..89b33fa 100644 --- a/tests/integration/gamma.rs +++ b/tests/integration/gamma.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn gamma() { +test_raster!(gamma, 500, 60, "tests/images/gamma.png", |pixmap| { let mut paint = Paint::default(); let stroke = Stroke::default(); let wide = Stroke { @@ -35,7 +34,6 @@ fn gamma() { ) .unwrap(); - let mut pixmap = Pixmap::new(500, 60).unwrap(); pixmap.fill(Color::BLACK); let mut pb = PathBuilder::new(); @@ -65,8 +63,4 @@ fn gamma() { paint.shader = grad3.clone(); pixmap.stroke_path(&path, &paint, &wide, xf, None); } - - // pixmap.save_png("tests/images/gamma.png").unwrap(); - let expected = Pixmap::load_png("tests/images/gamma.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/gradients.rs b/tests/integration/gradients.rs index 87d7af0..c885a9b 100644 --- a/tests/integration/gradients.rs +++ b/tests/integration/gradients.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn two_stops_linear_pad_lq() { +test_raster!(two_stops_linear_pad_lq, 200, 200, "tests/images/gradients/two-stops-linear-pad-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = LinearGradient::new( @@ -17,15 +16,11 @@ fn two_stops_linear_pad_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-pad-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_linear_repeat_lq() { +test_raster!(two_stops_linear_repeat_lq, 200, 200, "tests/images/gradients/two-stops-linear-repeat-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = LinearGradient::new( @@ -41,15 +36,11 @@ fn two_stops_linear_repeat_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-repeat-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_linear_reflect_lq() { +test_raster!(two_stops_linear_reflect_lq, 200, 200, "tests/images/gradients/two-stops-linear-reflect-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = LinearGradient::new( @@ -65,15 +56,11 @@ fn two_stops_linear_reflect_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-reflect-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn three_stops_evenly_spaced_lq() { +test_raster!(three_stops_evenly_spaced_lq, 200, 200, "tests/images/gradients/three-stops-evenly-spaced-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = LinearGradient::new( @@ -91,15 +78,11 @@ fn three_stops_evenly_spaced_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/three-stops-evenly-spaced-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_unevenly_spaced_lq() { +test_raster!(two_stops_unevenly_spaced_lq, 200, 200, "tests/images/gradients/two-stops-unevenly-spaced-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = LinearGradient::new( @@ -116,15 +99,11 @@ fn two_stops_unevenly_spaced_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-unevenly-spaced-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_linear_pad_hq() { +test_raster!(two_stops_linear_pad_hq, 200, 200, "tests/images/gradients/two-stops-linear-pad-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -141,15 +120,11 @@ fn two_stops_linear_pad_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-pad-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_linear_repeat_hq() { +test_raster!(two_stops_linear_repeat_hq, 200, 200, "tests/images/gradients/two-stops-linear-repeat-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -166,15 +141,11 @@ fn two_stops_linear_repeat_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-repeat-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_linear_reflect_hq() { +test_raster!(two_stops_linear_reflect_hq, 200, 200, "tests/images/gradients/two-stops-linear-reflect-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -191,15 +162,11 @@ fn two_stops_linear_reflect_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/two-stops-linear-reflect-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn three_stops_evenly_spaced_hq() { +test_raster!(three_stops_evenly_spaced_hq, 200, 200, "tests/images/gradients/three-stops-evenly-spaced-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -218,15 +185,11 @@ fn three_stops_evenly_spaced_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/three-stops-evenly-spaced-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn two_stops_unevenly_spaced_hq() { +test_raster!(two_stops_unevenly_spaced_hq, 200, 200, "tests/images/gradients/two-stops-unevenly-spaced-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -244,18 +207,14 @@ fn two_stops_unevenly_spaced_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/gradients/two-stops-unevenly-spaced-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); // The radial gradient is only supported by the high quality pipeline. // Therefore we do not have a lq/hq split. -#[test] -fn well_behaved_radial() { +test_raster!(well_behaved_radial, 200, 200, "tests/images/gradients/well-behaved-radial.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = RadialGradient::new( @@ -273,15 +232,11 @@ fn well_behaved_radial() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/well-behaved-radial.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn focal_on_circle_radial() { +test_raster!(focal_on_circle_radial, 200, 200, "tests/images/gradients/focal-on-circle-radial.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = RadialGradient::new( @@ -299,15 +254,11 @@ fn focal_on_circle_radial() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/focal-on-circle-radial.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn conical_greater_radial() { +test_raster!(conical_greater_radial, 200, 200, "tests/images/gradients/conical-greater-radial.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = RadialGradient::new( @@ -325,15 +276,11 @@ fn conical_greater_radial() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/conical-greater-radial.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn simple_radial_lq() { +test_raster!(simple_radial_lq, 200, 200, "tests/images/gradients/simple-radial-lq.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = RadialGradient::new( @@ -351,15 +298,11 @@ fn simple_radial_lq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/simple-radial-lq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn simple_radial_hq() { +test_raster!(simple_radial_hq, 200, 200, "tests/images/gradients/simple-radial-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -378,15 +321,11 @@ fn simple_radial_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/simple-radial-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn simple_radial_with_ts_hq() { +test_raster!(simple_radial_with_ts_hq, 200, 200, "tests/images/gradients/simple-radial-with-ts-hq.png", |pixmap| { let mut paint = Paint::default(); paint.force_hq_pipeline = true; paint.anti_alias = false; @@ -405,17 +344,13 @@ fn simple_radial_with_ts_hq() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/gradients/simple-radial-with-ts-hq.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); // Gradient doesn't add the Premultiply stage when all stops are opaque. // But it checks colors only on creation, so we have to recheck them after calling `apply_opacity`. -#[test] -fn global_opacity() { +test_raster!(global_opacity, 200, 200, "tests/images/gradients/global-opacity.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = RadialGradient::new( @@ -434,15 +369,11 @@ fn global_opacity() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/global-opacity.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn strip_gradient() { +test_raster!(strip_gradient, 200, 200, "tests/images/gradients/strip-gradient.png", |pixmap| { // Equal radii, different centers creates a Strip gradient let mut paint = Paint::default(); paint.anti_alias = false; @@ -461,15 +392,11 @@ fn strip_gradient() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/strip-gradient.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn concentric_radial() { +test_raster!(concentric_radial, 200, 200, "tests/images/gradients/concentric-radial.png", |pixmap| { // Same center, non-zero start radius (concentric gradient) let mut paint = Paint::default(); paint.anti_alias = false; @@ -488,15 +415,11 @@ fn concentric_radial() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/concentric-radial.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn conical_smaller_radial() { +test_raster!(conical_smaller_radial, 200, 200, "tests/images/gradients/conical-smaller-radial.png", |pixmap| { // Configuration that triggers XYTo2PtConicalSmaller stage // r0=60, r1=30, distance=50 // r0_norm=1.2, r1_norm=0.6, focal_x = 1.2/0.6 = 2.0 > 1.0 @@ -519,15 +442,11 @@ fn conical_smaller_radial() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/gradients/conical-smaller-radial.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn sweep_gradient() { +test_raster!(sweep_gradient, 200, 200, "tests/images/gradients/sweep-gradient.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = SweepGradient::new( @@ -545,7 +464,7 @@ fn sweep_gradient() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path( &path, &paint, @@ -553,13 +472,9 @@ fn sweep_gradient() { Transform::identity(), None, ); +}); - let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn sweep_gradient_full() { +test_raster!(sweep_gradient_full, 200, 200, "tests/images/gradients/sweep-gradient-full.png", |pixmap| { let mut paint = Paint::default(); paint.anti_alias = false; paint.shader = SweepGradient::new( @@ -577,7 +492,7 @@ fn sweep_gradient_full() { let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - let mut pixmap = Pixmap::new(200, 200).unwrap(); + pixmap.fill_path( &path, &paint, @@ -585,7 +500,4 @@ fn sweep_gradient_full() { Transform::identity(), None, ); - - let expected = Pixmap::load_png("tests/images/gradients/sweep-gradient-full.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/hairline.rs b/tests/integration/hairline.rs index c5828f4..c8f9bc0 100644 --- a/tests/integration/hairline.rs +++ b/tests/integration/hairline.rs @@ -1,7 +1,8 @@ use tiny_skia::*; +use crate::common::assert_pixmap_eq; -fn draw_line(x0: f32, y0: f32, x1: f32, y1: f32, anti_alias: bool, width: f32, line_cap: LineCap) -> Pixmap { - let mut pixmap = Pixmap::new(100, 100).unwrap(); +fn draw_line(x0: f32, y0: f32, x1: f32, y1: f32, anti_alias: bool, width: f32, line_cap: LineCap) -> PixmapGeneric

{ + let mut pixmap = PixmapGeneric::new(100, 100).unwrap(); let mut pb = PathBuilder::new(); pb.move_to(x0, y0); @@ -20,70 +21,35 @@ fn draw_line(x0: f32, y0: f32, x1: f32, y1: f32, anti_alias: bool, width: f32, l pixmap } -#[test] -fn hline_05() { - let expected = Pixmap::load_png("tests/images/hairline/hline-05.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 90.0, 10.0, false, 0.5, LineCap::Butt), expected); -} - -#[test] -fn hline_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/hline-05-aa.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 90.0, 10.0, true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn hline_05_aa_round() { - let expected = Pixmap::load_png("tests/images/hairline/hline-05-aa-round.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 90.0, 10.0, true, 0.5, LineCap::Round), expected); -} - -#[test] -fn vline_05() { - let expected = Pixmap::load_png("tests/images/hairline/vline-05.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 10.0, 90.0, false, 0.5, LineCap::Butt), expected); -} - -#[test] -fn vline_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/vline-05-aa.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 10.0, 90.0, true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn vline_05_aa_round() { - let expected = Pixmap::load_png("tests/images/hairline/vline-05-aa-round.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 10.0, 90.0, true, 0.5, LineCap::Round), expected); -} - -#[test] -fn horish_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/horish-05-aa.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 90.0, 70.0, true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn vertish_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/vertish-05-aa.png").unwrap(); - assert_eq!(draw_line(10.0, 10.0, 70.0, 90.0, true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn clip_line_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-line-05-aa.png").unwrap(); - assert_eq!(draw_line(-10.0, 10.0, 110.0, 70.0, true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn clip_line_00() { - let expected = Pixmap::load_png("tests/images/hairline/clip-line-00.png").unwrap(); - assert_eq!(draw_line(-10.0, 10.0, 110.0, 70.0, false, 0.0, LineCap::Butt), expected); -} - -#[test] -fn clip_line_00_v2() { - let mut pixmap = Pixmap::new(512, 512).unwrap(); - +macro_rules! test_hairline { + ($name:ident, $expected:expr, $draw:expr) => { + #[test] + fn $name() { + let expected = Pixmap::load_png($expected).unwrap(); + let p8: Pixmap = $draw; + assert_pixmap_eq(&p8, &expected, 0); + + #[cfg(feature = "16bpc")] + { + let p16: PixmapU16 = $draw; + assert_pixmap_eq(&p16, &expected, 3); + } + } + }; +} + +test_hairline!(hline_05, "tests/images/hairline/hline-05.png", draw_line(10.0, 10.0, 90.0, 10.0, false, 0.5, LineCap::Butt)); +test_hairline!(hline_05_aa, "tests/images/hairline/hline-05-aa.png", draw_line(10.0, 10.0, 90.0, 10.0, true, 0.5, LineCap::Butt)); +test_hairline!(hline_05_aa_round, "tests/images/hairline/hline-05-aa-round.png", draw_line(10.0, 10.0, 90.0, 10.0, true, 0.5, LineCap::Round)); +test_hairline!(vline_05, "tests/images/hairline/vline-05.png", draw_line(10.0, 10.0, 10.0, 90.0, false, 0.5, LineCap::Butt)); +test_hairline!(vline_05_aa, "tests/images/hairline/vline-05-aa.png", draw_line(10.0, 10.0, 10.0, 90.0, true, 0.5, LineCap::Butt)); +test_hairline!(vline_05_aa_round, "tests/images/hairline/vline-05-aa-round.png", draw_line(10.0, 10.0, 10.0, 90.0, true, 0.5, LineCap::Round)); +test_hairline!(horish_05_aa, "tests/images/hairline/horish-05-aa.png", draw_line(10.0, 10.0, 90.0, 70.0, true, 0.5, LineCap::Butt)); +test_hairline!(vertish_05_aa, "tests/images/hairline/vertish-05-aa.png", draw_line(10.0, 10.0, 70.0, 90.0, true, 0.5, LineCap::Butt)); +test_hairline!(clip_line_05_aa, "tests/images/hairline/clip-line-05-aa.png", draw_line(-10.0, 10.0, 110.0, 70.0, true, 0.5, LineCap::Butt)); +test_hairline!(clip_line_00, "tests/images/hairline/clip-line-00.png", draw_line(-10.0, 10.0, 110.0, 70.0, false, 0.0, LineCap::Butt)); + +test_raster!(clip_line_00_v2, 512, 512, "tests/images/hairline/clip-line-00-v2.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -96,37 +62,15 @@ fn clip_line_00_v2() { builder.line_to(488.0846, 471.04388); let path = builder.finish().unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/hairline/clip-line-00-v2.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn clip_hline_top_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-hline-top-aa.png").unwrap(); - assert_eq!(draw_line(-1.0, 0.0, 101.0, 0.0, true, 1.0, LineCap::Butt), expected); -} +test_hairline!(clip_hline_top_aa, "tests/images/hairline/clip-hline-top-aa.png", draw_line(-1.0, 0.0, 101.0, 0.0, true, 1.0, LineCap::Butt)); +test_hairline!(clip_hline_bottom_aa, "tests/images/hairline/clip-hline-bottom-aa.png", draw_line(-1.0, 100.0, 101.0, 100.0, true, 1.0, LineCap::Butt)); +test_hairline!(clip_vline_left_aa, "tests/images/hairline/clip-vline-left-aa.png", draw_line(0.0, -1.0, 0.0, 101.0, true, 1.0, LineCap::Butt)); +test_hairline!(clip_vline_right_aa, "tests/images/hairline/clip-vline-right-aa.png", draw_line(100.0, -1.0, 100.0, 101.0, true, 1.0, LineCap::Butt)); -#[test] -fn clip_hline_bottom_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-hline-bottom-aa.png").unwrap(); - assert_eq!(draw_line(-1.0, 100.0, 101.0, 100.0, true, 1.0, LineCap::Butt), expected); -} - -#[test] -fn clip_vline_left_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-vline-left-aa.png").unwrap(); - assert_eq!(draw_line(0.0, -1.0, 0.0, 101.0, true, 1.0, LineCap::Butt), expected); -} - -#[test] -fn clip_vline_right_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-vline-right-aa.png").unwrap(); - assert_eq!(draw_line(100.0, -1.0, 100.0, 101.0, true, 1.0, LineCap::Butt), expected); -} - -fn draw_quad(anti_alias: bool, width: f32, line_cap: LineCap) -> Pixmap { - let mut pixmap = Pixmap::new(200, 100).unwrap(); +fn draw_quad(anti_alias: bool, width: f32, line_cap: LineCap) -> PixmapGeneric

{ + let mut pixmap = PixmapGeneric::new(200, 100).unwrap(); let mut pb = PathBuilder::new(); pb.move_to(25.0, 80.0); @@ -145,26 +89,12 @@ fn draw_quad(anti_alias: bool, width: f32, line_cap: LineCap) -> Pixmap { pixmap } -#[test] -fn quad_width_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/quad-width-05-aa.png").unwrap(); - assert_eq!(draw_quad(true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn quad_width_05_aa_round() { - let expected = Pixmap::load_png("tests/images/hairline/quad-width-05-aa-round.png").unwrap(); - assert_eq!(draw_quad(true, 0.5, LineCap::Round), expected); -} - -#[test] -fn quad_width_00() { - let expected = Pixmap::load_png("tests/images/hairline/quad-width-00.png").unwrap(); - assert_eq!(draw_quad(false, 0.0, LineCap::Butt), expected); -} +test_hairline!(quad_width_05_aa, "tests/images/hairline/quad-width-05-aa.png", draw_quad(true, 0.5, LineCap::Butt)); +test_hairline!(quad_width_05_aa_round, "tests/images/hairline/quad-width-05-aa-round.png", draw_quad(true, 0.5, LineCap::Round)); +test_hairline!(quad_width_00, "tests/images/hairline/quad-width-00.png", draw_quad(false, 0.0, LineCap::Butt)); -fn draw_cubic(points: &[f32; 8], anti_alias: bool, width: f32, line_cap: LineCap) -> Pixmap { - let mut pixmap = Pixmap::new(200, 100).unwrap(); +fn draw_cubic(points: &[f32; 8], anti_alias: bool, width: f32, line_cap: LineCap) -> PixmapGeneric

{ + let mut pixmap = PixmapGeneric::new(200, 100).unwrap(); let mut pb = PathBuilder::new(); pb.move_to(points[0], points[1]); @@ -183,65 +113,17 @@ fn draw_cubic(points: &[f32; 8], anti_alias: bool, width: f32, line_cap: LineCap pixmap } -#[test] -fn cubic_width_10_aa() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-10-aa.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 1.0, LineCap::Butt), expected); -} - -#[test] -fn cubic_width_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-05-aa.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn cubic_width_00_aa() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-00-aa.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.0, LineCap::Butt), expected); -} - -#[test] -fn cubic_width_00() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-00.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Butt), expected); -} - -#[test] -fn cubic_width_05_aa_round() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-05-aa-round.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Round), expected); -} - -#[test] -fn cubic_width_00_round() { - let expected = Pixmap::load_png("tests/images/hairline/cubic-width-00-round.png").unwrap(); - assert_eq!(draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Round), expected); -} - -#[test] -fn chop_cubic_01() { - let expected = Pixmap::load_png("tests/images/hairline/chop-cubic-01.png").unwrap(); - // This curve will invoke `path_geometry::chop_cubic_at_max_curvature` branch of `hair_cubic`. - assert_eq!(draw_cubic(&[57.0, 13.0, 17.0, 15.0, 55.0, 97.0, 89.0, 62.0], true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn clip_cubic_05_aa() { - let expected = Pixmap::load_png("tests/images/hairline/clip-cubic-05-aa.png").unwrap(); - assert_eq!(draw_cubic(&[-25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Butt), expected); -} - -#[test] -fn clip_cubic_00() { - let expected = Pixmap::load_png("tests/images/hairline/clip-cubic-00.png").unwrap(); - assert_eq!(draw_cubic(&[-25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Butt), expected); -} - -#[test] -fn clipped_circle_aa() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); +test_hairline!(cubic_width_10_aa, "tests/images/hairline/cubic-width-10-aa.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 1.0, LineCap::Butt)); +test_hairline!(cubic_width_05_aa, "tests/images/hairline/cubic-width-05-aa.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Butt)); +test_hairline!(cubic_width_00_aa, "tests/images/hairline/cubic-width-00-aa.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.0, LineCap::Butt)); +test_hairline!(cubic_width_00, "tests/images/hairline/cubic-width-00.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Butt)); +test_hairline!(cubic_width_05_aa_round, "tests/images/hairline/cubic-width-05-aa-round.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Round)); +test_hairline!(cubic_width_00_round, "tests/images/hairline/cubic-width-00-round.png", draw_cubic(&[25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Round)); +test_hairline!(chop_cubic_01, "tests/images/hairline/chop-cubic-01.png", draw_cubic(&[57.0, 13.0, 17.0, 15.0, 55.0, 97.0, 89.0, 62.0], true, 0.5, LineCap::Butt)); +test_hairline!(clip_cubic_05_aa, "tests/images/hairline/clip-cubic-05-aa.png", draw_cubic(&[-25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], true, 0.5, LineCap::Butt)); +test_hairline!(clip_cubic_00, "tests/images/hairline/clip-cubic-00.png", draw_cubic(&[-25.0, 80.0, 55.0, 25.0, 155.0, 75.0, 175.0, 20.0], false, 0.0, LineCap::Butt)); +test_raster!(clipped_circle_aa, 100, 100, "tests/images/hairline/clipped-circle-aa.png", tolerance: 3, |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -251,7 +133,4 @@ fn clipped_circle_aa() { let path = PathBuilder::from_circle(50.0, 50.0, 55.0).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/hairline/clipped-circle-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/main.rs b/tests/integration/main.rs index 088091a..2c16251 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod common; + #[rustfmt::skip] mod mask; #[rustfmt::skip] mod dash; #[rustfmt::skip] mod fill; @@ -10,3 +13,5 @@ #[rustfmt::skip] mod png; #[rustfmt::skip] mod skia_dash; #[rustfmt::skip] mod stroke; +#[cfg(feature = "16bpc")] +#[rustfmt::skip] mod u16_precision; diff --git a/tests/integration/mask.rs b/tests/integration/mask.rs index 6a48ae2..264dd1f 100644 --- a/tests/integration/mask.rs +++ b/tests/integration/mask.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn rect() { +test_raster!(rect, 100, 100, "tests/images/mask/rect.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); let mut mask = Mask::new(100, 100).unwrap(); mask.fill_path(&clip_path, FillRule::Winding, false, Transform::default()); @@ -10,16 +9,11 @@ fn rect() { paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; - let mut pixmap = Pixmap::new(100, 100).unwrap(); let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), Some(&mask)); +}); - let expected = Pixmap::load_png("tests/images/mask/rect.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn rect_aa() { +test_raster!(rect_aa, 100, 100, "tests/images/mask/rect-aa.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.5, 10.0, 80.0, 80.5).unwrap()); let mut mask = Mask::new(100, 100).unwrap(); mask.fill_path(&clip_path, FillRule::Winding, true, Transform::default()); @@ -28,18 +22,11 @@ fn rect_aa() { paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; - let mut pixmap = Pixmap::new(100, 100).unwrap(); let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), Some(&mask)); +}); - let expected = Pixmap::load_png("tests/images/mask/rect-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn rect_ts() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); - +test_raster!(rect_ts, 100, 100, "tests/images/mask/rect-ts.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); let clip_path = clip_path.transform(Transform::from_row(1.0, -0.3, 0.0, 1.0, 0.0, 15.0)).unwrap(); @@ -52,15 +39,9 @@ fn rect_ts() { let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), Some(&mask)); +}); - let expected = Pixmap::load_png("tests/images/mask/rect-ts.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn circle_bottom_right_aa() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); - +test_raster!(circle_bottom_right_aa, 100, 100, "tests/images/mask/circle-bottom-right-aa.png", |pixmap| { let clip_path = PathBuilder::from_circle(100.0, 100.0, 50.0).unwrap(); let mut mask = Mask::new(100, 100).unwrap(); mask.fill_path(&clip_path, FillRule::Winding, true, Transform::default()); @@ -71,15 +52,9 @@ fn circle_bottom_right_aa() { let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), Some(&mask)); +}); - let expected = Pixmap::load_png("tests/images/mask/circle-bottom-right-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn stroke() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); - +test_raster!(stroke, 100, 100, "tests/images/mask/stroke.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); let mut mask = Mask::new(100, 100).unwrap(); mask.fill_path(&clip_path, FillRule::Winding, false, Transform::default()); @@ -93,16 +68,10 @@ fn stroke() { let path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), Some(&mask)); - - let expected = Pixmap::load_png("tests/images/mask/stroke.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); // Make sure we're clipping only source and not source and destination -#[test] -fn skip_dest() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); - +test_raster!(skip_dest, 100, 100, "tests/images/mask/skip-dest.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; @@ -115,8 +84,8 @@ fn skip_dest() { None, ); - let mut pixmap2 = Pixmap::new(200, 200).unwrap(); - pixmap2.as_mut().fill_path( + let mut pixmap2 = PixmapGeneric::new(200, 200).unwrap(); + pixmap2.fill_path( &PathBuilder::from_rect(Rect::from_xywh(35.0, 35.0, 60.0, 60.0).unwrap()), &paint, FillRule::Winding, @@ -130,13 +99,9 @@ fn skip_dest() { pixmap.draw_pixmap(0, 0, pixmap2.as_ref(), &PixmapPaint::default(), Transform::identity(), Some(&mask)); +}); - let expected = Pixmap::load_png("tests/images/mask/skip-dest.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn intersect_aa() { +test_raster!(intersect_aa, 200, 200, "tests/images/mask/intersect-aa.png", |pixmap| { let circle1 = PathBuilder::from_circle(75.0, 75.0, 50.0).unwrap(); let circle2 = PathBuilder::from_circle(125.0, 125.0, 50.0).unwrap(); @@ -148,20 +113,15 @@ fn intersect_aa() { paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = false; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_rect( Rect::from_xywh(0.0, 0.0, 200.0, 200.0).unwrap(), &paint, Transform::identity(), Some(&mask), ); +}); - let expected = Pixmap::load_png("tests/images/mask/intersect-aa.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn ignore_memset() { +test_raster!(ignore_memset, 100, 100, "tests/images/mask/ignore-memset.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); let mut mask = Mask::new(100, 100).unwrap(); @@ -171,20 +131,15 @@ fn ignore_memset() { paint.set_color_rgba8(50, 127, 150, 255); paint.anti_alias = false; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill_rect( Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), &paint, Transform::identity(), Some(&mask), ); +}); - let expected = Pixmap::load_png("tests/images/mask/ignore-memset.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn ignore_source() { +test_raster!(ignore_source, 100, 100, "tests/images/mask/ignore-source.png", |pixmap| { let clip_path = PathBuilder::from_rect(Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap()); let mut mask = Mask::new(100, 100).unwrap(); @@ -195,7 +150,6 @@ fn ignore_source() { paint.blend_mode = BlendMode::SourceOver; paint.anti_alias = false; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.fill(Color::WHITE); pixmap.fill_rect( Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), @@ -203,15 +157,9 @@ fn ignore_source() { Transform::identity(), Some(&mask), ); +}); - let expected = Pixmap::load_png("tests/images/mask/ignore-source.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn apply_mask() { - let mut pixmap = Pixmap::new(100, 100).unwrap(); - +test_raster!(apply_mask, 100, 100, "tests/images/mask/apply-mask.png", |pixmap| { let clip_path = PathBuilder::from_circle(100.0, 100.0, 50.0).unwrap(); let mut mask = Mask::new(100, 100).unwrap(); mask.fill_path(&clip_path, FillRule::Winding, true, Transform::default()); @@ -223,10 +171,7 @@ fn apply_mask() { let rect = Rect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(); pixmap.fill_rect(rect, &paint, Transform::identity(), None); pixmap.apply_mask(&mask); - - let expected = Pixmap::load_png("tests/images/mask/apply-mask.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); #[test] fn mask_from_alpha() { @@ -243,6 +188,14 @@ fn mask_from_alpha() { let expected = Mask::load_png("tests/images/mask/mask-from-alpha.png").unwrap(); assert_eq!(mask, expected); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(100, 100).unwrap(); + pixmap16.fill_path(&path, &paint, FillRule::Winding, Transform::default(), None); + let mask16 = Mask::from_pixmap(pixmap16.as_ref(), MaskType::Alpha); + crate::common::assert_mask_eq(&mask16, &expected, 1); + } } #[test] @@ -260,4 +213,12 @@ fn mask_from_luma() { let expected = Mask::load_png("tests/images/mask/mask-from-luma.png").unwrap(); assert_eq!(mask, expected); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(100, 100).unwrap(); + pixmap16.fill_path(&path, &paint, FillRule::Winding, Transform::default(), None); + let mask16 = Mask::from_pixmap(pixmap16.as_ref(), MaskType::Luminance); + crate::common::assert_mask_eq(&mask16, &expected, 1); + } } diff --git a/tests/integration/pattern.rs b/tests/integration/pattern.rs index 9e00b45..13d9c8f 100644 --- a/tests/integration/pattern.rs +++ b/tests/integration/pattern.rs @@ -1,6 +1,6 @@ use tiny_skia::*; -fn crate_triangle() -> Pixmap { +fn create_triangle(_target: &PixmapGeneric

) -> PixmapGeneric

{ let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -12,18 +12,17 @@ fn crate_triangle() -> Pixmap { pb.close(); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(20, 20).unwrap(); + let mut pixmap = PixmapGeneric::new(20, 20).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); pixmap } -#[test] -fn pad_nearest() { - let triangle = crate_triangle(); +test_raster!(pad_nearest, 200, 200, "tests/images/pattern/pad-nearest.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Pad, FilterQuality::Nearest, @@ -32,21 +31,15 @@ fn pad_nearest() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/pad-nearest.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn repeat_nearest() { - let triangle = crate_triangle(); +test_raster!(repeat_nearest, 200, 200, "tests/images/pattern/repeat-nearest.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Nearest, @@ -55,21 +48,15 @@ fn repeat_nearest() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/repeat-nearest.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn reflect_nearest() { - let triangle = crate_triangle(); +test_raster!(reflect_nearest, 200, 200, "tests/images/pattern/reflect-nearest.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Reflect, FilterQuality::Nearest, @@ -78,95 +65,66 @@ fn reflect_nearest() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/reflect-nearest.png").unwrap(); - assert_eq!(pixmap, expected); -} - -// We have to test tile mode for bilinear/bicubic separately, -// because they're using a different algorithm from nearest. -#[test] -fn pad_bicubic() { - let triangle = crate_triangle(); +test_raster!(pad_bicubic, 200, 200, "tests/images/pattern/pad-bicubic.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Pad, FilterQuality::Bicubic, 1.0, - // Transform must be set, otherwise we will fallback to Nearest. Transform::from_row(1.1, 0.3, 0.0, 1.4, 0.0, 0.0), ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/pad-bicubic.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn repeat_bicubic() { - let triangle = crate_triangle(); +test_raster!(repeat_bicubic, 200, 200, "tests/images/pattern/repeat-bicubic.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Bicubic, 1.0, - // Transform must be set, otherwise we will fallback to Nearest. Transform::from_row(1.1, 0.3, 0.0, 1.4, 0.0, 0.0), ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/repeat-bicubic.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn reflect_bicubic() { - let triangle = crate_triangle(); +test_raster!(reflect_bicubic, 200, 200, "tests/images/pattern/reflect-bicubic.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Reflect, FilterQuality::Bicubic, 1.0, - // Transform must be set, otherwise we will fallback to Nearest. Transform::from_row(1.1, 0.3, 0.0, 1.4, 0.0, 0.0), ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/reflect-bicubic.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn filter_nearest_no_ts() { - let triangle = crate_triangle(); +test_raster!(filter_nearest_no_ts, 200, 200, "tests/images/pattern/filter-nearest-no-ts.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Nearest, @@ -175,21 +133,15 @@ fn filter_nearest_no_ts() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/filter-nearest-no-ts.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn filter_nearest() { - let triangle = crate_triangle(); +test_raster!(filter_nearest, 200, 200, "tests/images/pattern/filter-nearest.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Nearest, @@ -198,21 +150,15 @@ fn filter_nearest() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/filter-nearest.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn filter_bilinear() { - let triangle = crate_triangle(); +test_raster!(filter_bilinear, 200, 200, "tests/images/pattern/filter-bilinear.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Bilinear, @@ -221,21 +167,15 @@ fn filter_bilinear() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/pattern/filter-bilinear.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn filter_bicubic() { - let triangle = crate_triangle(); +test_raster!(filter_bicubic, 200, 200, "tests/images/pattern/filter-bicubic.png", |pixmap| { + let triangle = create_triangle(&pixmap); let mut paint = Paint::default(); paint.anti_alias = false; - paint.shader = Pattern::new( + paint.shader = Pattern::from_pixmap( triangle.as_ref(), SpreadMode::Repeat, FilterQuality::Bicubic, @@ -244,10 +184,5 @@ fn filter_bicubic() { ); let path = PathBuilder::from_rect(Rect::from_ltrb(10.0, 10.0, 190.0, 190.0).unwrap()); - - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - - let expected = Pixmap::load_png("tests/images/pattern/filter-bicubic.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/pixmap.rs b/tests/integration/pixmap.rs index 1dda753..6aa9206 100644 --- a/tests/integration/pixmap.rs +++ b/tests/integration/pixmap.rs @@ -2,12 +2,13 @@ use tiny_skia::*; #[test] fn clone_rect_1() { - let mut pixmap = Pixmap::new(200, 200).unwrap(); - let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; + let expected = Pixmap::load_png("tests/images/pixmap/clone-rect-1.png").unwrap(); + + let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path( &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), &paint, @@ -17,19 +18,33 @@ fn clone_rect_1() { ); let part = pixmap.as_ref().clone_rect(IntRect::from_xywh(10, 15, 80, 90).unwrap()).unwrap(); - - let expected = Pixmap::load_png("tests/images/pixmap/clone-rect-1.png").unwrap(); - assert_eq!(part, expected); + crate::common::assert_pixmap_eq(&part, &expected, 0); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(200, 200).unwrap(); + pixmap16.fill_path( + &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let part16 = pixmap16.as_ref().clone_rect(IntRect::from_xywh(10, 15, 80, 90).unwrap()).unwrap(); + crate::common::assert_pixmap_eq(&part16, &expected, 2); + } } #[test] fn clone_rect_2() { - let mut pixmap = Pixmap::new(200, 200).unwrap(); - let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; + let expected = Pixmap::load_png("tests/images/pixmap/clone-rect-2.png").unwrap(); + + let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path( &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), &paint, @@ -39,19 +54,31 @@ fn clone_rect_2() { ); let part = pixmap.as_ref().clone_rect(IntRect::from_xywh(130, 120, 80, 90).unwrap()).unwrap(); - - let expected = Pixmap::load_png("tests/images/pixmap/clone-rect-2.png").unwrap(); - assert_eq!(part, expected); + crate::common::assert_pixmap_eq(&part, &expected, 0); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(200, 200).unwrap(); + pixmap16.fill_path( + &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + let part16 = pixmap16.as_ref().clone_rect(IntRect::from_xywh(130, 120, 80, 90).unwrap()).unwrap(); + crate::common::assert_pixmap_eq(&part16, &expected, 2); + } } #[test] fn clone_rect_out_of_bound() { - let mut pixmap = Pixmap::new(200, 200).unwrap(); - let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; + let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.fill_path( &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), &paint, @@ -63,6 +90,22 @@ fn clone_rect_out_of_bound() { assert!(pixmap.as_ref().clone_rect(IntRect::from_xywh(250, 15, 80, 90).unwrap()).is_none()); assert!(pixmap.as_ref().clone_rect(IntRect::from_xywh(10, 250, 80, 90).unwrap()).is_none()); assert!(pixmap.as_ref().clone_rect(IntRect::from_xywh(10, -250, 80, 90).unwrap()).is_none()); + + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(200, 200).unwrap(); + pixmap16.fill_path( + &PathBuilder::from_circle(100.0, 100.0, 80.0).unwrap(), + &paint, + FillRule::Winding, + Transform::identity(), + None, + ); + + assert!(pixmap16.as_ref().clone_rect(IntRect::from_xywh(250, 15, 80, 90).unwrap()).is_none()); + assert!(pixmap16.as_ref().clone_rect(IntRect::from_xywh(10, 250, 80, 90).unwrap()).is_none()); + assert!(pixmap16.as_ref().clone_rect(IntRect::from_xywh(10, -250, 80, 90).unwrap()).is_none()); + } } #[test] @@ -71,13 +114,16 @@ fn fill() { let mut pixmap = Pixmap::new(10, 10).unwrap(); pixmap.fill(c); assert_eq!(pixmap.pixel(1, 1).unwrap(), c.premultiply().to_color_u8()); -} -#[test] -fn draw_pixmap() { - // Tests that painting algorithm will switch `Bicubic`/`Bilinear` to `Nearest`. - // Otherwise we will get a blurry image. + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(10, 10).unwrap(); + pixmap16.fill(c); + assert_eq!(pixmap16.pixel(1, 1).unwrap(), c.premultiply().to_color_u16()); + } +} +test_raster!(draw_pixmap, 200, 200, "tests/images/canvas/draw-pixmap.png", |pixmap| { // A pixmap with the bottom half filled with solid color. let sub_pixmap = { let mut paint = Paint::default(); @@ -86,23 +132,18 @@ fn draw_pixmap() { let rect = Rect::from_xywh(0.0, 50.0, 100.0, 50.0).unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); - pixmap.fill_rect(rect, &paint, Transform::identity(), None); - pixmap + let mut sub = PixmapGeneric::new(100, 100).unwrap(); + sub.fill_rect(rect, &paint, Transform::identity(), None); + sub }; let mut paint = PixmapPaint::default(); paint.quality = FilterQuality::Bicubic; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.draw_pixmap(20, 20, sub_pixmap.as_ref(), &paint, Transform::identity(), None); +}); - let expected = Pixmap::load_png("tests/images/canvas/draw-pixmap.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn draw_pixmap_ts() { +test_raster!(draw_pixmap_ts, 200, 200, "tests/images/canvas/draw-pixmap-ts.png", |pixmap| { let triangle = { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); @@ -115,15 +156,14 @@ fn draw_pixmap_ts() { pb.close(); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); - pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - pixmap + let mut sub = PixmapGeneric::new(100, 100).unwrap(); + sub.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + sub }; let mut paint = PixmapPaint::default(); paint.quality = FilterQuality::Bicubic; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.draw_pixmap( 5, 10, triangle.as_ref(), @@ -131,13 +171,9 @@ fn draw_pixmap_ts() { Transform::from_row(1.2, 0.5, 0.5, 1.2, 0.0, 0.0), None, ); +}); - let expected = Pixmap::load_png("tests/images/canvas/draw-pixmap-ts.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn draw_pixmap_opacity() { +test_raster!(draw_pixmap_opacity, 200, 200, "tests/images/canvas/draw-pixmap-opacity.png", |pixmap| { let triangle = { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); @@ -150,16 +186,15 @@ fn draw_pixmap_opacity() { pb.close(); let path = pb.finish().unwrap(); - let mut pixmap = Pixmap::new(100, 100).unwrap(); - pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); - pixmap + let mut sub = PixmapGeneric::new(100, 100).unwrap(); + sub.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); + sub }; let mut paint = PixmapPaint::default(); paint.quality = FilterQuality::Bicubic; paint.opacity = 0.5; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.draw_pixmap( 5, 10, triangle.as_ref(), @@ -167,7 +202,4 @@ fn draw_pixmap_opacity() { Transform::from_row(1.2, 0.5, 0.5, 1.2, 0.0, 0.0), None, ); - - let expected = Pixmap::load_png("tests/images/canvas/draw-pixmap-opacity.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/skia_dash.rs b/tests/integration/skia_dash.rs index d3a8362..654a122 100644 --- a/tests/integration/skia_dash.rs +++ b/tests/integration/skia_dash.rs @@ -41,5 +41,9 @@ fn infinite_dash() { let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); // Doesn't draw anything. - assert!(true); + #[cfg(feature = "16bpc")] + { + let mut pixmap16 = PixmapU16::new(100, 100).unwrap(); + pixmap16.stroke_path(&path, &paint, &stroke, Transform::identity(), None); + } } diff --git a/tests/integration/stroke.rs b/tests/integration/stroke.rs index bed748f..dcf9f96 100644 --- a/tests/integration/stroke.rs +++ b/tests/integration/stroke.rs @@ -1,7 +1,6 @@ use tiny_skia::*; -#[test] -fn round_caps_and_large_scale() { +test_raster!(round_caps_and_large_scale, 200, 200, "tests/images/stroke/round-caps-and-large-scale.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -19,15 +18,10 @@ fn round_caps_and_large_scale() { let transform = Transform::from_scale(16.0, 16.0); - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, transform, None); +}); - let expected = Pixmap::load_png("tests/images/stroke/round-caps-and-large-scale.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn circle() { +test_raster!(circle, 200, 200, "tests/images/stroke/circle.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -36,15 +30,10 @@ fn circle() { let mut stroke = Stroke::default(); stroke.width = 2.0; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::default(), None); +}); - let expected = Pixmap::load_png("tests/images/stroke/circle.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn zero_len_subpath_butt_cap() { +test_raster!(zero_len_subpath_butt_cap, 100, 100, "tests/images/stroke/zero-len-subpath-butt-cap.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -58,15 +47,10 @@ fn zero_len_subpath_butt_cap() { stroke.width = 20.0; stroke.line_cap = LineCap::Butt; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::default(), None); +}); - let expected = Pixmap::load_png("tests/images/stroke/zero-len-subpath-butt-cap.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn zero_len_subpath_round_cap() { +test_raster!(zero_len_subpath_round_cap, 100, 100, "tests/images/stroke/zero-len-subpath-round-cap.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -80,15 +64,10 @@ fn zero_len_subpath_round_cap() { stroke.width = 20.0; stroke.line_cap = LineCap::Round; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::default(), None); +}); - let expected = Pixmap::load_png("tests/images/stroke/zero-len-subpath-round-cap.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn zero_len_subpath_square_cap() { +test_raster!(zero_len_subpath_square_cap, 100, 100, "tests/images/stroke/zero-len-subpath-square-cap.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -102,15 +81,10 @@ fn zero_len_subpath_square_cap() { stroke.width = 20.0; stroke.line_cap = LineCap::Square; - let mut pixmap = Pixmap::new(100, 100).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::default(), None); +}); - let expected = Pixmap::load_png("tests/images/stroke/zero-len-subpath-square-cap.png").unwrap(); - assert_eq!(pixmap, expected); -} - -#[test] -fn round_cap_join() { +test_raster!(round_cap_join, 200, 200, "tests/images/stroke/round-cap-join.png", |pixmap| { let mut paint = Paint::default(); paint.set_color_rgba8(50, 127, 150, 200); paint.anti_alias = true; @@ -126,9 +100,5 @@ fn round_cap_join() { stroke.line_cap = LineCap::Round; stroke.line_join = LineJoin::Round; - let mut pixmap = Pixmap::new(200, 200).unwrap(); pixmap.stroke_path(&path, &paint, &stroke, Transform::default(), None); - - let expected = Pixmap::load_png("tests/images/stroke/round-cap-join.png").unwrap(); - assert_eq!(pixmap, expected); -} +}); diff --git a/tests/integration/u16_precision.rs b/tests/integration/u16_precision.rs new file mode 100644 index 0000000..e4f86ab --- /dev/null +++ b/tests/integration/u16_precision.rs @@ -0,0 +1,101 @@ +use tiny_skia::*; + +#[test] +fn solid_fill_and_draw() { + let mut pixmap = PixmapU16::new(100, 100).unwrap(); + assert_eq!(pixmap.width(), 100); + assert_eq!(pixmap.height(), 100); + + // Fill with semi-transparent red + let color = Color::from_rgba(1.0, 0.0, 0.0, 0.5).unwrap(); + let mut paint = Paint::default(); + paint.set_color(color); + + let rect = Rect::from_xywh(10.0, 10.0, 80.0, 80.0).unwrap(); + pixmap.fill_rect(rect, &paint, Transform::identity(), None); + + // Check center pixel: premultiplied alpha = 32768, red = 32768, green = 0, blue = 0 + let pixel = pixmap.pixel(50, 50).unwrap(); + assert_eq!(pixel.alpha(), 32768); + assert_eq!(pixel.red(), 32768); + assert_eq!(pixel.green(), 0); + assert_eq!(pixel.blue(), 0); + + // Check outside pixel: transparent + let pixel_out = pixmap.pixel(5, 5).unwrap(); + assert_eq!(pixel_out, PremultipliedColorU16::TRANSPARENT); +} + +#[test] +fn continuous_gradient_sub_8bit_steps() { + let mut pixmap = PixmapU16::new(512, 1).unwrap(); + + // Subtle gradient spanning from #000000 (0) to #010101 (257 / 65535 in 16-bit) + let c0 = Color::from_rgba(0.0, 0.0, 0.0, 1.0).unwrap(); + let c1 = Color::from_rgba(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0, 1.0).unwrap(); + + let shader = LinearGradient::new( + Point::from_xy(0.0, 0.0), + Point::from_xy(512.0, 0.0), + vec![ + GradientStop::new(0.0, c0), + GradientStop::new(1.0, c1), + ], + SpreadMode::Pad, + Transform::identity(), + ).unwrap(); + + let mut paint = Paint::default(); + paint.shader = shader; + + pixmap.fill_rect( + Rect::from_xywh(0.0, 0.0, 512.0, 1.0).unwrap(), + &paint, + Transform::identity(), + None, + ); + + // Assert that we have intermediate values strictly between 0 and 257 + let mut found_intermediate = false; + let mut intermediate_values = Vec::new(); + + for x in 0..512 { + let pixel = pixmap.pixel(x, 0).unwrap(); + let r = pixel.red(); + if r > 0 && r < 257 { + found_intermediate = true; + intermediate_values.push(r); + } + } + + assert!(found_intermediate, "Pipeline must generate true 16-bit intermediate steps between 0 and 257"); + // Verify that we have multiple distinct intermediate steps + intermediate_values.dedup(); + assert!( + intermediate_values.len() > 10, + "Expected multiple distinct sub-8-bit gradient steps, got {}", + intermediate_values.len() + ); +} + +#[test] +fn path_and_hairline_rendering() { + let mut pixmap = PixmapU16::new(64, 64).unwrap(); + + let mut pb = PathBuilder::new(); + pb.move_to(10.0, 10.0); + pb.line_to(54.0, 54.0); + let path = pb.finish().unwrap(); + + let mut paint = Paint::default(); + paint.set_color(Color::from_rgba(0.0, 1.0, 0.0, 1.0).unwrap()); + + let stroke = Stroke::default(); + pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None); + + // Assert stroke was rendered + let p_mid = pixmap.pixel(32, 32).unwrap(); + assert!(p_mid.green() > 30000, "Green channel should be drawn on stroke line"); + assert_eq!(p_mid.red(), 0); + assert_eq!(p_mid.blue(), 0); +}