From fd8572989e4b0bbcf3e6bae19c3a7980391362fc Mon Sep 17 00:00:00 2001 From: echosoar <82163514@qq.com> Date: Mon, 30 Mar 2026 09:01:48 +0800 Subject: [PATCH 1/2] fix: y --- Cargo.lock | 80 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 ++- src/path.rs | 8 ++-- src/png.rs | 2 +- src/render.rs | 8 ++-- tests/image_220x200.png | Bin 0 -> 37490 bytes tests/integration_test.rs | 60 ++++++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 tests/image_220x200.png diff --git a/Cargo.lock b/Cargo.lock index 0bb3230..7e5b25b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,7 +1,85 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "canvas-rs" version = "0.1.0" +dependencies = [ + "png", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[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 = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" diff --git a/Cargo.toml b/Cargo.toml index 2b932bd..02ffaf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "canvas-rs" version = "0.1.0" -edition = "2024" +edition = "2021" [dependencies] + +[dev-dependencies] +png = "0.17" diff --git a/src/path.rs b/src/path.rs index 1266444..51f7379 100644 --- a/src/path.rs +++ b/src/path.rs @@ -77,10 +77,10 @@ impl Path { } current.clear(); // pen stays at the start of the closed sub-path (web spec) - if let Some(sp) = sub_paths.last() - && let Some(&p) = sp.first() - { - pen = p; + if let Some(sp) = sub_paths.last() { + if let Some(&p) = sp.first() { + pen = p; + } } } } diff --git a/src/png.rs b/src/png.rs index 302e57c..84e8728 100644 --- a/src/png.rs +++ b/src/png.rs @@ -150,7 +150,7 @@ const B64_ALPHABET: &[u8] = /// Encode `data` to standard base-64 (with `=` padding). pub fn base64_encode(data: &[u8]) -> String { - let mut out = String::with_capacity(data.len().div_ceil(3) * 4); + let mut out = String::with_capacity((data.len() + 2) / 3 * 4); let mut chunks = data.chunks_exact(3); for chunk in chunks.by_ref() { let n = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | (chunk[2] as u32); diff --git a/src/render.rs b/src/render.rs index e648547..4eb40fd 100644 --- a/src/render.rs +++ b/src/render.rs @@ -25,10 +25,10 @@ pub fn put_pixel( return; } let idx = (y as u32 * width + x as u32) as usize; - if let Some(mask) = clip - && !mask[idx] - { - return; + if let Some(mask) = clip { + if !mask[idx] { + return; + } } let base = idx * 4; let dst = Color::rgba(buf[base], buf[base + 1], buf[base + 2], buf[base + 3]); diff --git a/tests/image_220x200.png b/tests/image_220x200.png new file mode 100644 index 0000000000000000000000000000000000000000..b4b85f64f24b86e2ec4e5b7e39012e76d18b44fe GIT binary patch literal 37490 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%0A$r&aM=_llQWu#LAZcutLdpt? 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 ImageData { + let file = std::fs::File::open(path).expect("could not open PNG file"); + let decoder = png::Decoder::new(file); + let mut reader = decoder.read_info().expect("could not read PNG info"); + let mut buf = vec![0u8; reader.output_buffer_size()]; + let frame = reader.next_frame(&mut buf).expect("could not decode PNG frame"); + let bytes = buf[..frame.buffer_size()].to_vec(); + let (w, h) = (frame.width, frame.height); + let rgba = match frame.color_type { + png::ColorType::Rgba => bytes, + png::ColorType::Rgb => bytes + .chunks(3) + .flat_map(|p| [p[0], p[1], p[2], 255u8]) + .collect(), + _ => panic!("unsupported PNG color type: {:?}", frame.color_type), + }; + ImageData::from_rgba(w, h, rgba) +} + +#[test] +fn fill_rect_green_then_draw_image() { + let canvas = Canvas::new(300, 300); + let mut ctx = canvas.get_context("2d").unwrap(); + + // Draw green rectangle (CSS "green" = rgb(0, 128, 0)). + ctx.set_fill_style("green"); + ctx.fill_rect(10.0, 10.0, 150.0, 100.0); + + // Verify the rectangle is painted before the image is drawn on top. + let snapshot = canvas.get_image_data(); + let green_px = snapshot.get_pixel(50, 50); + assert_eq!(green_px.r, 0, "green rect r should be 0"); + assert_eq!(green_px.g, 128, "green rect g should be 128"); + assert_eq!(green_px.b, 0, "green rect b should be 0"); + assert_eq!(green_px.a, 255, "green rect should be fully opaque"); + + // Pixel outside the rect should still be transparent at this point. + let outside_snap = snapshot.get_pixel(5, 5); + assert_eq!(outside_snap.a, 0, "pixel outside rect should be transparent before image draw"); + + // Load tests/image_220x200.png and draw it on top of the canvas. + let img_data = load_png_rgba("tests/image_220x200.png"); + assert_eq!(img_data.width, 220, "loaded image width should be 220"); + assert_eq!(img_data.height, 200, "loaded image height should be 200"); + ctx.draw_image(&img_data, 0.0, 0.0); + + // After drawing, a pixel outside both the image (220×200) and the green + // rect should still be transparent. + let result = canvas.get_image_data(); + let far_px = result.get_pixel(260, 260); + assert_eq!(far_px.a, 0, "pixel outside image and rect should remain transparent"); + + // The canvas should export to a valid PNG data URL without panicking. + let url = canvas.to_data_url(); + assert!(url.starts_with("data:image/png;base64,"), "export should produce a valid PNG data URL"); +} From cf5455ef92affca8e36aa4bb9fe90340cd43fd8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:58:26 +0000 Subject: [PATCH 2/2] refactor: split canvas-rs into canvas and images workspace crates Agent-Logs-Url: https://github.com/echosoar/canvas-rs/sessions/872c790f-1c54-4484-bcc8-0facd661cd5b Co-authored-by: echosoar <14832743+echosoar@users.noreply.github.com> --- Cargo.lock | 15 ++-- Cargo.toml | 15 ++-- canvas/Cargo.toml | 6 ++ {src => canvas/src}/canvas.rs | 28 ++------ {src => canvas/src}/color.rs | 0 {src => canvas/src}/image.rs | 0 {src => canvas/src}/lib.rs | 11 ++- {src => canvas/src}/path.rs | 0 {src => canvas/src}/render.rs | 0 images/Cargo.toml | 8 +++ src/png.rs => images/src/encoder.rs | 0 images/src/lib.rs | 75 ++++++++++++++++++++ {tests => images/tests}/image_220x200.png | Bin {tests => images/tests}/integration_test.rs | 74 +++++++++---------- 14 files changed, 149 insertions(+), 83 deletions(-) create mode 100644 canvas/Cargo.toml rename {src => canvas/src}/canvas.rs (92%) rename {src => canvas/src}/color.rs (100%) rename {src => canvas/src}/image.rs (100%) rename {src => canvas/src}/lib.rs (77%) rename {src => canvas/src}/path.rs (100%) rename {src => canvas/src}/render.rs (100%) create mode 100644 images/Cargo.toml rename src/png.rs => images/src/encoder.rs (100%) create mode 100644 images/src/lib.rs rename {tests => images/tests}/image_220x200.png (100%) rename {tests => images/tests}/integration_test.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index 7e5b25b..559cebc 100644 --- a/Cargo.lock +++ b/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 = "adler2" @@ -15,11 +15,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "canvas-rs" +name = "canvas" version = "0.1.0" -dependencies = [ - "png", -] [[package]] name = "cfg-if" @@ -55,6 +52,14 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "images" +version = "0.1.0" +dependencies = [ + "canvas", + "png", +] + [[package]] name = "miniz_oxide" version = "0.8.9" diff --git a/Cargo.toml b/Cargo.toml index 02ffaf1..b53383c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,6 @@ -[package] -name = "canvas-rs" -version = "0.1.0" -edition = "2021" - -[dependencies] - -[dev-dependencies] -png = "0.17" +[workspace] +members = [ + "canvas", + "images", +] +resolver = "2" diff --git a/canvas/Cargo.toml b/canvas/Cargo.toml new file mode 100644 index 0000000..d7ebe49 --- /dev/null +++ b/canvas/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "canvas" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/src/canvas.rs b/canvas/src/canvas.rs similarity index 92% rename from src/canvas.rs rename to canvas/src/canvas.rs index 0616022..d7b17d2 100644 --- a/src/canvas.rs +++ b/canvas/src/canvas.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use crate::color::{parse_color, Color}; use crate::image::ImageData; use crate::path::{Path, PathCommand}; -use crate::png::{base64_encode, encode_png}; use crate::render::{self, LineCap}; // ── Canvas ─────────────────────────────────────────────────────────────────── @@ -12,13 +11,12 @@ use crate::render::{self, LineCap}; /// A 2-D drawing surface, analogous to the HTML `` element. /// /// ``` -/// use canvas_rs::Canvas; +/// use canvas::Canvas; /// /// let canvas = Canvas::new(100, 100); /// let mut ctx = canvas.get_context("2d").unwrap(); /// ctx.set_fill_style("red"); /// ctx.fill_rect(0.0, 0.0, 100.0, 100.0); -/// let _url = canvas.to_data_url(); /// ``` pub struct Canvas { pub(crate) width: u32, @@ -56,25 +54,6 @@ impl Canvas { }) } - /// Encode the canvas contents as a `data:image/png;base64,...` URL. - /// - /// The optional `type_` parameter is accepted for API compatibility but - /// only `"image/png"` is supported. The `quality` parameter is ignored - /// for PNG. - pub fn to_data_url(&self) -> String { - self.to_data_url_with_options("image/png", 1.0) - } - - pub fn to_data_url_with_type(&self, type_: &str) -> String { - self.to_data_url_with_options(type_, 1.0) - } - - pub fn to_data_url_with_options(&self, _type_: &str, _quality: f64) -> String { - let buf = self.buffer.borrow(); - let png = encode_png(self.width, self.height, &buf); - format!("data:image/png;base64,{}", base64_encode(&png)) - } - /// Return the canvas width in pixels. pub fn width(&self) -> u32 { self.width @@ -89,6 +68,11 @@ impl Canvas { pub fn get_image_data(&self) -> ImageData { ImageData::from_rgba(self.width, self.height, self.buffer.borrow().clone()) } + + /// Borrow the raw RGBA pixel buffer. + pub fn pixels(&self) -> std::cell::Ref<'_, Vec> { + self.buffer.borrow() + } } // ── Context2D ──────────────────────────────────────────────────────────────── diff --git a/src/color.rs b/canvas/src/color.rs similarity index 100% rename from src/color.rs rename to canvas/src/color.rs diff --git a/src/image.rs b/canvas/src/image.rs similarity index 100% rename from src/image.rs rename to canvas/src/image.rs diff --git a/src/lib.rs b/canvas/src/lib.rs similarity index 77% rename from src/lib.rs rename to canvas/src/lib.rs index 85efc2a..90c1fbf 100644 --- a/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,11 +1,13 @@ //! Pure-Rust 2-D drawing library with a web-canvas-like API. //! -//! No external dependencies are required. +//! No external dependencies are required. All drawing operations work on an +//! in-memory RGBA pixel buffer. To encode or decode PNG images, use the +//! companion `images` crate. //! //! # Quick start //! //! ``` -//! use canvas_rs::Canvas; +//! use canvas::Canvas; //! //! // Create a 200×100 canvas. //! let canvas = Canvas::new(200, 100); @@ -20,17 +22,12 @@ //! ctx.begin_path(); //! ctx.arc(100.0, 50.0, 40.0, 0.0, std::f64::consts::PI * 2.0, false); //! ctx.fill(); -//! -//! // Export as data URL. -//! let url = canvas.to_data_url(); -//! assert!(url.starts_with("data:image/png;base64,")); //! ``` pub mod canvas; pub mod color; pub mod image; pub mod path; -pub mod png; pub mod render; pub use canvas::{Canvas, Context2D}; diff --git a/src/path.rs b/canvas/src/path.rs similarity index 100% rename from src/path.rs rename to canvas/src/path.rs diff --git a/src/render.rs b/canvas/src/render.rs similarity index 100% rename from src/render.rs rename to canvas/src/render.rs diff --git a/images/Cargo.toml b/images/Cargo.toml new file mode 100644 index 0000000..134d3a2 --- /dev/null +++ b/images/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "images" +version = "0.1.0" +edition = "2021" + +[dependencies] +canvas = { path = "../canvas" } +png = "0.17" diff --git a/src/png.rs b/images/src/encoder.rs similarity index 100% rename from src/png.rs rename to images/src/encoder.rs diff --git a/images/src/lib.rs b/images/src/lib.rs new file mode 100644 index 0000000..f9aa63d --- /dev/null +++ b/images/src/lib.rs @@ -0,0 +1,75 @@ +//! PNG encoding and decoding for the `canvas` crate. +//! +//! This crate wraps the core `canvas` RGBA buffer with PNG import/export +//! capabilities. +//! +//! # Quick start +//! +//! ```no_run +//! use canvas::Canvas; +//! +//! let mut canvas = Canvas::new(200, 100); +//! let mut ctx = canvas.get_context("2d").unwrap(); +//! ctx.set_fill_style("red"); +//! ctx.fill_rect(0.0, 0.0, 200.0, 100.0); +//! +//! // Export to a data URL. +//! let url = images::to_data_url(&canvas); +//! assert!(url.starts_with("data:image/png;base64,")); +//! +//! // Or get raw PNG bytes. +//! let png_bytes: Vec = images::to_blob(&canvas); +//! ``` + +pub mod encoder; + +pub use encoder::{base64_encode, encode_png}; + +use canvas::{Canvas, ImageData}; + +/// Encode a `Canvas` as raw PNG bytes. +pub fn to_blob(canvas: &Canvas) -> Vec { + let buf = canvas.pixels(); + encode_png(canvas.width(), canvas.height(), &buf) +} + +/// Encode a `Canvas` as a `data:image/png;base64,...` URL. +pub fn to_data_url(canvas: &Canvas) -> String { + format!("data:image/png;base64,{}", base64_encode(&to_blob(canvas))) +} + +/// Decode a PNG byte slice into an [`ImageData`] with RGBA pixels. +/// +/// Returns an error string if the bytes cannot be decoded as a valid PNG. +pub fn from_png(bytes: &[u8]) -> Result { + use png::ColorType; + use std::io::Cursor; + + let decoder = png::Decoder::new(Cursor::new(bytes)); + let mut reader = decoder + .read_info() + .map_err(|e| format!("PNG read_info error: {e}"))?; + let mut buf = vec![0u8; reader.output_buffer_size()]; + let frame = reader + .next_frame(&mut buf) + .map_err(|e| format!("PNG decode error: {e}"))?; + let raw = buf[..frame.buffer_size()].to_vec(); + let (w, h) = (frame.width, frame.height); + let rgba = match frame.color_type { + ColorType::Rgba => raw, + ColorType::Rgb => raw + .chunks(3) + .flat_map(|p| [p[0], p[1], p[2], 255u8]) + .collect(), + ColorType::Grayscale => raw + .iter() + .flat_map(|&v| [v, v, v, 255u8]) + .collect(), + ColorType::GrayscaleAlpha => raw + .chunks(2) + .flat_map(|p| [p[0], p[0], p[0], p[1]]) + .collect(), + other => return Err(format!("unsupported PNG color type: {other:?}")), + }; + Ok(ImageData::from_rgba(w, h, rgba)) +} diff --git a/tests/image_220x200.png b/images/tests/image_220x200.png similarity index 100% rename from tests/image_220x200.png rename to images/tests/image_220x200.png diff --git a/tests/integration_test.rs b/images/tests/integration_test.rs similarity index 91% rename from tests/integration_test.rs rename to images/tests/integration_test.rs index b400bbf..30092e1 100644 --- a/tests/integration_test.rs +++ b/images/tests/integration_test.rs @@ -1,4 +1,4 @@ -use canvas_rs::{Canvas, Color, ImageData}; +use canvas::{Canvas, Color, ImageData}; use std::f64::consts::PI; // ── Canvas creation ────────────────────────────────────────────────────────── @@ -37,26 +37,18 @@ fn get_context_unknown_returns_none() { #[test] fn to_data_url_starts_with_correct_prefix() { let canvas = Canvas::new(8, 8); - let url = canvas.to_data_url(); + let url = images::to_data_url(&canvas); assert!( url.starts_with("data:image/png;base64,"), "URL should start with data:image/png;base64," ); } -#[test] -fn to_data_url_with_options_respects_type() { - let canvas = Canvas::new(4, 4); - let url = canvas.to_data_url_with_options("image/png", 1.0); - assert!(url.starts_with("data:image/png;base64,")); -} - #[test] fn to_data_url_png_header_valid() { let canvas = Canvas::new(2, 2); - let url = canvas.to_data_url(); + let url = images::to_data_url(&canvas); let b64 = url.strip_prefix("data:image/png;base64,").unwrap(); - // Decode first few bytes manually to check the PNG signature. // PNG signature in base64 starts with "iVBOR". assert!( b64.starts_with("iVBOR"), @@ -64,6 +56,15 @@ fn to_data_url_png_header_valid() { ); } +// ── to_blob ────────────────────────────────────────────────────────────────── + +#[test] +fn to_blob_is_valid_png() { + let canvas = Canvas::new(4, 4); + let bytes = images::to_blob(&canvas); + assert_eq!(&bytes[..8], &[137, 80, 78, 71, 13, 10, 26, 10], "to_blob should return a valid PNG"); +} + // ── fillStyle / strokeStyle properties ─────────────────────────────────────── #[test] @@ -454,7 +455,7 @@ fn alpha_blending_on_fill_rect() { #[test] fn color_hex_short() { - use canvas_rs::color::parse_color; + use canvas::color::parse_color; assert_eq!(parse_color("#f00"), Some(Color::rgb(255, 0, 0))); assert_eq!(parse_color("#0f0"), Some(Color::rgb(0, 255, 0))); assert_eq!(parse_color("#00f"), Some(Color::rgb(0, 0, 255))); @@ -462,13 +463,13 @@ fn color_hex_short() { #[test] fn color_hex_long() { - use canvas_rs::color::parse_color; + use canvas::color::parse_color; assert_eq!(parse_color("#ff0000"), Some(Color::rgb(255, 0, 0))); } #[test] fn color_named_all_common() { - use canvas_rs::color::parse_color; + use canvas::color::parse_color; let colors = ["red","green","blue","white","black","transparent", "yellow","cyan","magenta","orange","purple","pink", "gray","silver","lime","navy","teal","coral","gold"]; @@ -481,47 +482,39 @@ fn color_named_all_common() { #[test] fn png_encode_nonempty() { - use canvas_rs::png::encode_png; + use images::encode_png; let pixels = vec![255u8, 0, 0, 255, 0, 255, 0, 255]; // 2×1 (red, green) let png = encode_png(2, 1, &pixels); // Must start with PNG signature. assert_eq!(&png[..8], &[137, 80, 78, 71, 13, 10, 26, 10]); - // Must contain the IEND tag (4 bytes length=0, then "IEND", then 4-byte CRC). - // Find "IEND" in the file. + // Must contain the IEND tag. let has_iend = png.windows(4).any(|w| w == b"IEND"); assert!(has_iend, "PNG should contain IEND chunk"); } #[test] fn base64_round_trip() { - use canvas_rs::png::base64_encode; + use images::base64_encode; let original = b"Hello, World!"; let encoded = base64_encode(original); assert_eq!(encoded, "SGVsbG8sIFdvcmxkIQ=="); } -// ── fill_rect + draw image from file ───────────────────────────────────────── +// ── from_png (PNG decoding) ─────────────────────────────────────────────────── -/// Decode a PNG file from `path` and return it as an `ImageData` (RGBA). -fn load_png_rgba(path: &str) -> ImageData { - let file = std::fs::File::open(path).expect("could not open PNG file"); - let decoder = png::Decoder::new(file); - let mut reader = decoder.read_info().expect("could not read PNG info"); - let mut buf = vec![0u8; reader.output_buffer_size()]; - let frame = reader.next_frame(&mut buf).expect("could not decode PNG frame"); - let bytes = buf[..frame.buffer_size()].to_vec(); - let (w, h) = (frame.width, frame.height); - let rgba = match frame.color_type { - png::ColorType::Rgba => bytes, - png::ColorType::Rgb => bytes - .chunks(3) - .flat_map(|p| [p[0], p[1], p[2], 255u8]) - .collect(), - _ => panic!("unsupported PNG color type: {:?}", frame.color_type), - }; - ImageData::from_rgba(w, h, rgba) +#[test] +fn from_png_roundtrip() { + // Encode a canvas, then decode it back and check dimensions. + let canvas = Canvas::new(8, 8); + let png_bytes = images::to_blob(&canvas); + let img = images::from_png(&png_bytes).expect("round-trip PNG decode should succeed"); + assert_eq!(img.width, 8); + assert_eq!(img.height, 8); + assert_eq!(img.data.len(), 8 * 8 * 4); } +// ── fill_rect + draw image from file ───────────────────────────────────────── + #[test] fn fill_rect_green_then_draw_image() { let canvas = Canvas::new(300, 300); @@ -543,8 +536,9 @@ fn fill_rect_green_then_draw_image() { let outside_snap = snapshot.get_pixel(5, 5); assert_eq!(outside_snap.a, 0, "pixel outside rect should be transparent before image draw"); - // Load tests/image_220x200.png and draw it on top of the canvas. - let img_data = load_png_rgba("tests/image_220x200.png"); + // Load tests/image_220x200.png using images::from_png and draw it on top. + let png_bytes = std::fs::read("tests/image_220x200.png").expect("could not read PNG file"); + let img_data = images::from_png(&png_bytes).expect("could not decode PNG"); assert_eq!(img_data.width, 220, "loaded image width should be 220"); assert_eq!(img_data.height, 200, "loaded image height should be 200"); ctx.draw_image(&img_data, 0.0, 0.0); @@ -556,6 +550,6 @@ fn fill_rect_green_then_draw_image() { assert_eq!(far_px.a, 0, "pixel outside image and rect should remain transparent"); // The canvas should export to a valid PNG data URL without panicking. - let url = canvas.to_data_url(); + let url = images::to_data_url(&canvas); assert!(url.starts_with("data:image/png;base64,"), "export should produce a valid PNG data URL"); }