1use std::path::{Path, PathBuf};
73
74use ab_glyph::{Font, FontVec, Glyph, PxScale, ScaleFont};
75use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
76use thiserror::Error;
77
78#[derive(Debug, Error)]
84pub enum ImageError {
85 #[error("Failed to open image: {0}")]
87 OpenFailed(String),
88
89 #[error("Failed to save image: {0}")]
91 SaveFailed(String),
92
93 #[error("Unsupported image type: {0}")]
95 UnsupportedType(String),
96
97 #[error("Invalid color: {0}")]
99 InvalidColor(String),
100
101 #[error("Failed to load font: {0}")]
103 FontLoadFailed(String),
104
105 #[error(transparent)]
107 Io(#[from] std::io::Error),
108
109 #[error(transparent)]
111 Decode(#[from] image::ImageError),
112
113 #[error("{0}")]
115 InvalidArgument(String),
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum ImageType {
136 #[default]
138 Unknown,
139 Gif,
141 Jpeg,
143 Png,
145 Wbmp,
147}
148
149impl ImageType {
150 pub fn as_str(self) -> &'static str {
152 match self {
153 ImageType::Unknown => "",
154 ImageType::Gif => "GIF",
155 ImageType::Jpeg => "JPEG",
156 ImageType::Png => "PNG",
157 ImageType::Wbmp => "WBMP",
158 }
159 }
160
161 pub fn from_extension(ext: &str) -> Self {
163 match ext.to_lowercase().as_str() {
164 "gif" => ImageType::Gif,
165 "jpg" | "jpeg" => ImageType::Jpeg,
166 "png" => ImageType::Png,
167 "wbmp" => ImageType::Wbmp,
168 _ => ImageType::Unknown,
169 }
170 }
171
172 pub fn from_image_format(format: image::ImageFormat) -> Self {
174 match format {
175 image::ImageFormat::Gif => ImageType::Gif,
176 image::ImageFormat::Jpeg => ImageType::Jpeg,
177 image::ImageFormat::Png => ImageType::Png,
178 image::ImageFormat::WebP => ImageType::Unknown, _ => ImageType::Unknown,
180 }
181 }
182
183 pub fn to_image_format(self) -> Option<image::ImageFormat> {
185 match self {
186 ImageType::Gif => Some(image::ImageFormat::Gif),
187 ImageType::Jpeg => Some(image::ImageFormat::Jpeg),
188 ImageType::Png => Some(image::ImageFormat::Png),
189 ImageType::Wbmp => None, ImageType::Unknown => None,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213pub struct Color {
214 pub r: u8,
216 pub g: u8,
218 pub b: u8,
220 pub a: u8,
225}
226
227impl Color {
228 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
230 Self { r, g, b, a: 255 }
231 }
232
233 pub fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
237 Self { r, g, b, a }
238 }
239
240 pub fn from_hex(hex: &str) -> Result<Self, ImageError> {
249 let hex = hex.trim().trim_start_matches('#');
250 let parse = |s: &str| {
251 u8::from_str_radix(s, 16).map_err(|_| ImageError::InvalidColor(hex.to_string()))
252 };
253 let (r, g, b, a) = match hex.len() {
254 3 => {
255 let r = parse(&format!("{}{}", &hex[0..1], &hex[0..1]))?;
257 let g = parse(&format!("{}{}", &hex[1..2], &hex[1..2]))?;
258 let b = parse(&format!("{}{}", &hex[2..3], &hex[2..3]))?;
259 (r, g, b, 255u8)
260 }
261 4 => {
262 let r = parse(&format!("{}{}", &hex[0..1], &hex[0..1]))?;
264 let g = parse(&format!("{}{}", &hex[1..2], &hex[1..2]))?;
265 let b = parse(&format!("{}{}", &hex[2..3], &hex[2..3]))?;
266 let a = parse(&format!("{}{}", &hex[3..4], &hex[3..4]))?;
267 (r, g, b, a)
268 }
269 6 => {
270 let r = parse(&hex[0..2])?;
272 let g = parse(&hex[2..4])?;
273 let b = parse(&hex[4..6])?;
274 (r, g, b, 255u8)
275 }
276 8 => {
277 let r = parse(&hex[0..2])?;
279 let g = parse(&hex[2..4])?;
280 let b = parse(&hex[4..6])?;
281 let a = parse(&hex[6..8])?;
282 (r, g, b, a)
283 }
284 _ => return Err(ImageError::InvalidColor(hex.to_string())),
285 };
286 Ok(Self { r, g, b, a })
287 }
288
289 pub fn to_rgba(self) -> Rgba<u8> {
291 Rgba([self.r, self.g, self.b, self.a])
292 }
293}
294
295impl Default for Color {
296 fn default() -> Self {
297 Self::rgb(0, 0, 0)
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum Position {
313 TopLeft,
315 TopCenter,
317 TopRight,
319 CenterLeft,
321 Center,
323 CenterRight,
325 BottomLeft,
327 BottomCenter,
329 BottomRight,
331}
332
333impl Position {
334 pub fn parse(s: &str) -> Result<Self, ImageError> {
336 match s.to_lowercase().as_str() {
337 "top-left" => Ok(Self::TopLeft),
338 "top-center" => Ok(Self::TopCenter),
339 "top-right" => Ok(Self::TopRight),
340 "center-left" => Ok(Self::CenterLeft),
341 "center" => Ok(Self::Center),
342 "center-right" => Ok(Self::CenterRight),
343 "bottom-left" => Ok(Self::BottomLeft),
344 "bottom-center" => Ok(Self::BottomCenter),
345 "bottom-right" => Ok(Self::BottomRight),
346 _ => Err(ImageError::InvalidArgument(format!(
347 "Unknown position: {s}"
348 ))),
349 }
350 }
351
352 pub fn as_str(self) -> &'static str {
354 match self {
355 Self::TopLeft => "top-left",
356 Self::TopCenter => "top-center",
357 Self::TopRight => "top-right",
358 Self::CenterLeft => "center-left",
359 Self::Center => "center",
360 Self::CenterRight => "center-right",
361 Self::BottomLeft => "bottom-left",
362 Self::BottomCenter => "bottom-center",
363 Self::BottomRight => "bottom-right",
364 }
365 }
366
367 pub fn get_xy(self, w1: u32, h1: u32, w2: u32, h2: u32) -> (i32, i32) {
375 let w1 = w1 as i32;
376 let h1 = h1 as i32;
377 let w2 = w2 as i32;
378 let h2 = h2 as i32;
379 let x = match self {
380 Self::TopLeft | Self::CenterLeft | Self::BottomLeft => 0,
381 Self::TopCenter | Self::Center | Self::BottomCenter => (w1 - w2) / 2,
382 Self::TopRight | Self::CenterRight | Self::BottomRight => w1 - w2,
383 };
384 let y = match self {
385 Self::TopLeft | Self::TopCenter | Self::TopRight => 0,
386 Self::CenterLeft | Self::Center | Self::CenterRight => (h1 - h2) / 2,
387 Self::BottomLeft | Self::BottomCenter | Self::BottomRight => h1 - h2,
388 };
389 (x, y)
390 }
391}
392
393#[derive(Debug)]
402pub struct Image {
403 dyn_image: DynamicImage,
405 file_path: Option<PathBuf>,
407 image_type: ImageType,
409}
410
411impl Image {
412 pub async fn open<P: AsRef<Path>>(path: P) -> Result<Self, ImageError> {
417 let path = path.as_ref();
418 let dyn_image = image::open(path)?;
419 let image_type = guess_image_type(path)?;
420 Ok(Self {
421 dyn_image,
422 file_path: Some(path.to_path_buf()),
423 image_type,
424 })
425 }
426
427 pub fn create_blank(width: u32, height: u32) -> Self {
432 let image: RgbaImage = ImageBuffer::new(width, height);
433 Self {
434 dyn_image: DynamicImage::ImageRgba8(image),
435 file_path: None,
436 image_type: ImageType::Unknown,
437 }
438 }
439
440 pub fn from_dynamic(dyn_image: DynamicImage, image_type: ImageType) -> Self {
442 Self {
443 dyn_image,
444 file_path: None,
445 image_type,
446 }
447 }
448
449 pub fn width(&self) -> u32 {
451 self.dyn_image.width()
452 }
453
454 pub fn height(&self) -> u32 {
456 self.dyn_image.height()
457 }
458
459 pub fn image_type(&self) -> ImageType {
461 self.image_type
462 }
463
464 pub fn file_path(&self) -> Option<&Path> {
466 self.file_path.as_deref()
467 }
468
469 pub fn as_dynamic(&self) -> &DynamicImage {
471 &self.dyn_image
472 }
473
474 pub fn as_dynamic_mut(&mut self) -> &mut DynamicImage {
476 &mut self.dyn_image
477 }
478
479 pub fn to_rgba8(&self) -> RgbaImage {
481 self.dyn_image.to_rgba8()
482 }
483
484 pub fn from_rgba8(image: RgbaImage, image_type: ImageType) -> Self {
486 Self {
487 dyn_image: DynamicImage::ImageRgba8(image),
488 file_path: None,
489 image_type,
490 }
491 }
492}
493
494fn guess_image_type(path: &Path) -> Result<ImageType, ImageError> {
496 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
498 let from_ext = ImageType::from_extension(ext);
499 if from_ext != ImageType::Unknown {
500 return Ok(from_ext);
501 }
502 let format = image::ImageReader::open(path)
504 .map_err(|e| ImageError::OpenFailed(format!("{path:?}: {e}")))?
505 .with_guessed_format()
506 .map_err(|e| ImageError::OpenFailed(format!("{path:?}: {e}")))?
507 .format()
508 .ok_or_else(|| ImageError::UnsupportedType("Unknown image format".to_string()))?;
509 Ok(ImageType::from_image_format(format))
510}
511
512pub struct Editor;
530
531impl Editor {
532 pub fn new() -> Self {
534 Self
535 }
536
537 pub async fn open<P: AsRef<Path>>(&self, path: P) -> Result<Image, ImageError> {
542 Image::open(path).await
543 }
544
545 pub fn resize_exact(&self, image: &mut Image, new_width: u32, new_height: u32) {
550 let resized = image::imageops::resize(
551 image.as_dynamic(),
552 new_width,
553 new_height,
554 image::imageops::FilterType::Lanczos3,
555 );
556 image.dyn_image = DynamicImage::ImageRgba8(resized);
557 }
558
559 pub fn resize_fit(&self, image: &mut Image, new_width: u32, new_height: u32) {
563 let (w, h) = (image.width(), image.height());
564 let ratio = (new_width as f64 / w as f64).min(new_height as f64 / h as f64);
565 let target_w = (w as f64 * ratio).round() as u32;
566 let target_h = (h as f64 * ratio).round() as u32;
567 let resized = image::imageops::resize(
568 image.as_dynamic(),
569 target_w,
570 target_h,
571 image::imageops::FilterType::Lanczos3,
572 );
573 image.dyn_image = DynamicImage::ImageRgba8(resized);
574 }
575
576 pub fn resize_fill(&self, image: &mut Image, new_width: u32, new_height: u32) {
580 let (w, h) = (image.width(), image.height());
581 let ratio = (new_width as f64 / w as f64).max(new_height as f64 / h as f64);
582 let scaled_w = (w as f64 * ratio).round() as u32;
583 let scaled_h = (h as f64 * ratio).round() as u32;
584 let scaled = image::imageops::resize(
586 image.as_dynamic(),
587 scaled_w,
588 scaled_h,
589 image::imageops::FilterType::Lanczos3,
590 );
591 let x = (scaled_w - new_width) / 2;
593 let y = (scaled_h - new_height) / 2;
594 let cropped = image::imageops::crop_imm(&scaled, x, y, new_width, new_height).to_image();
595 image.dyn_image = DynamicImage::ImageRgba8(cropped);
596 }
597
598 pub fn resize_exact_width(&self, image: &mut Image, new_width: u32) {
600 let h = image.height();
601 let new_height = (h as f64 * (new_width as f64 / image.width() as f64)).round() as u32;
602 self.resize_exact(image, new_width, new_height);
603 }
604
605 pub fn resize_exact_height(&self, image: &mut Image, new_height: u32) {
607 let w = image.width();
608 let new_width = (w as f64 * (new_height as f64 / image.height() as f64)).round() as u32;
609 self.resize_exact(image, new_width, new_height);
610 }
611
612 pub fn crop(
616 &self,
617 image: &mut Image,
618 crop_width: u32,
619 crop_height: u32,
620 position: Position,
621 offset_x: i32,
622 offset_y: i32,
623 ) -> Result<(), ImageError> {
624 let (w, h) = (image.width(), image.height());
625 if crop_width > w || crop_height > h {
626 return Err(ImageError::InvalidArgument(format!(
627 "crop size {crop_width}x{crop_height} larger than image {w}x{h}"
628 )));
629 }
630 let (mut x, mut y) = position.get_xy(w, h, crop_width, crop_height);
631 x += offset_x;
632 y += offset_y;
633 let x = x.max(0) as u32;
635 let y = y.max(0) as u32;
636 let x = x.min(w - crop_width);
637 let y = y.min(h - crop_height);
638 let cropped =
639 image::imageops::crop_imm(image.as_dynamic(), x, y, crop_width, crop_height).to_image();
640 image.dyn_image = DynamicImage::ImageRgba8(cropped);
641 Ok(())
642 }
643
644 #[allow(clippy::too_many_arguments)]
655 pub fn blend(
656 &self,
657 image1: &mut Image,
658 image2: &Image,
659 blend_type: BlendType,
660 opacity: f32,
661 position: Position,
662 offset_x: i32,
663 offset_y: i32,
664 ) -> Result<(), ImageError> {
665 let (w1, h1) = (image1.width(), image1.height());
666 let (w2, h2) = (image2.width(), image2.height());
667 let (base_x, base_y) = position.get_xy(w1, h1, w2, h2);
668 let x = base_x + offset_x;
669 let y = base_y + offset_y;
670
671 let mut base = image1.to_rgba8();
673 let overlay = image2.to_rgba8();
674
675 match blend_type {
676 BlendType::Normal => {
677 blend_normal(&mut base, &overlay, x, y, opacity);
678 }
679 BlendType::Multiply => {
680 blend_multiply(&mut base, &overlay, x, y, opacity);
681 }
682 BlendType::Overlay => {
683 blend_overlay(&mut base, &overlay, x, y, opacity);
684 }
685 BlendType::Screen => {
686 blend_screen(&mut base, &overlay, x, y, opacity);
687 }
688 }
689
690 image1.dyn_image = DynamicImage::ImageRgba8(base);
691 Ok(())
692 }
693
694 #[allow(clippy::too_many_arguments)]
707 pub async fn text(
708 &self,
709 image: &mut Image,
710 text: &str,
711 size: u32,
712 x: i32,
713 y: i32,
714 color: Color,
715 font_path: Option<&Path>,
716 ) -> Result<(), ImageError> {
717 let font = load_font(font_path).await?;
718 let rust_y = y - size as i32;
720 let mut rgba_image = image.to_rgba8();
721 let scale = PxScale::from(size as f32);
722 imageproc::drawing::draw_text_mut(
723 &mut rgba_image,
724 color.to_rgba(),
725 x,
726 rust_y,
727 scale,
728 &font,
729 text,
730 );
731 image.dyn_image = DynamicImage::ImageRgba8(rgba_image);
732 Ok(())
733 }
734
735 pub fn rotate(&self, image: &mut Image, angle: f32) -> Result<(), ImageError> {
741 let angle = angle.rem_euclid(360.0);
743 let rotated = match angle as i32 {
744 0 => image.dyn_image.clone(),
745 90 | -270 => image.dyn_image.rotate90(),
746 180 | -180 => image.dyn_image.rotate180(),
747 270 | -90 => image.dyn_image.rotate270(),
748 _ => {
749 return Err(ImageError::InvalidArgument(format!(
750 "rotate only supports 0/90/180/270 degrees, got {angle}"
751 )))
752 }
753 };
754 image.dyn_image = rotated;
755 Ok(())
756 }
757
758 pub fn flip(&self, image: &mut Image, mode: FlipMode) {
762 match mode {
763 FlipMode::Horizontal => image.dyn_image = image.dyn_image.fliph(),
764 FlipMode::Vertical => image.dyn_image = image.dyn_image.flipv(),
765 }
766 }
767
768 pub fn fill(&self, image: &mut Image, color: Color) {
773 let (w, h) = (image.width(), image.height());
774 let pixel = color.to_rgba();
775 let mut buf: RgbaImage = ImageBuffer::new(w, h);
776 for y in 0..h {
777 for x in 0..w {
778 buf.put_pixel(x, y, pixel);
779 }
780 }
781 image.dyn_image = DynamicImage::ImageRgba8(buf);
782 }
783
784 pub async fn save(
795 &self,
796 image: &Image,
797 file: &Path,
798 image_type: Option<ImageType>,
799 quality: Option<u8>,
800 _interlace: bool,
801 permission: u32,
802 ) -> Result<(), ImageError> {
803 let _ = &permission;
805 let save_type = image_type.unwrap_or_else(|| {
807 let ext = file.extension().and_then(|s| s.to_str()).unwrap_or("");
809 let t = ImageType::from_extension(ext);
810 if t != ImageType::Unknown {
811 t
812 } else {
813 image.image_type()
814 }
815 });
816
817 if let Some(parent) = file.parent() {
819 if !parent.as_os_str().is_empty() && !parent.exists() {
820 tokio::fs::create_dir_all(parent).await?;
821 #[cfg(unix)]
822 {
823 use std::os::unix::fs::PermissionsExt;
824 let _ = tokio::fs::set_permissions(
825 parent,
826 std::fs::Permissions::from_mode(permission),
827 )
828 .await;
829 }
830 }
831 }
832
833 match save_type {
835 ImageType::Png => {
836 image.as_dynamic().save(file)?;
837 }
838 ImageType::Jpeg => {
839 let q = quality.unwrap_or(75);
841 let q = q.clamp(1, 100);
842 let rgba = image.to_rgba8();
843 let rgb = image::DynamicImage::ImageRgba8(rgba).to_rgb8();
844 let mut buf = Vec::new();
845 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, q);
846 encoder.encode_image(&image::DynamicImage::ImageRgb8(rgb))?;
847 tokio::fs::write(file, buf).await?;
848 }
849 ImageType::Gif => {
850 image.as_dynamic().save(file)?;
851 }
852 ImageType::Wbmp => {
853 return Err(ImageError::UnsupportedType(
854 "WBMP encoding not supported by image crate".to_string(),
855 ));
856 }
857 ImageType::Unknown => {
858 return Err(ImageError::UnsupportedType(format!(
859 "Cannot determine save type for file: {file:?}"
860 )));
861 }
862 }
863 Ok(())
864 }
865}
866
867impl Default for Editor {
868 fn default() -> Self {
869 Self::new()
870 }
871}
872
873#[derive(Debug, Clone, Copy, PartialEq, Eq)]
879pub enum BlendType {
880 Normal,
882 Multiply,
884 Overlay,
886 Screen,
888}
889
890impl BlendType {
891 pub fn parse(s: &str) -> Result<Self, ImageError> {
893 match s.to_lowercase().as_str() {
894 "normal" => Ok(Self::Normal),
895 "multiply" => Ok(Self::Multiply),
896 "overlay" => Ok(Self::Overlay),
897 "screen" => Ok(Self::Screen),
898 _ => Err(ImageError::InvalidArgument(format!(
899 "Unknown blend type: {s}"
900 ))),
901 }
902 }
903}
904
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911pub enum FlipMode {
912 Horizontal,
914 Vertical,
916}
917
918impl FlipMode {
919 pub fn parse(s: &str) -> Result<Self, ImageError> {
921 match s.to_lowercase().as_str() {
922 "h" => Ok(Self::Horizontal),
923 "v" => Ok(Self::Vertical),
924 _ => Err(ImageError::InvalidArgument(format!(
925 "Unknown flip mode: {s}"
926 ))),
927 }
928 }
929}
930
931fn blend_normal(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
939 let (w1, h1) = base.dimensions();
940 let (w2, h2) = overlay.dimensions();
941 let opacity = opacity.clamp(0.0, 1.0);
942
943 for oy in 0..h2 {
944 for ox in 0..w2 {
945 let bx = x + ox as i32;
946 let by = y + oy as i32;
947 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
948 continue;
949 }
950 let src = overlay.get_pixel(ox, oy);
951 let dst = base.get_pixel(bx as u32, by as u32);
952 let src_alpha = (src[3] as f32 / 255.0) * opacity;
954 if src_alpha < 1e-6 {
955 continue;
956 }
957 let dst_alpha = dst[3] as f32 / 255.0;
958 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
960 if out_alpha < 1e-6 {
961 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
962 continue;
963 }
964 let out_r = ((src[0] as f32 * src_alpha
966 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
967 / out_alpha) as u8;
968 let out_g = ((src[1] as f32 * src_alpha
969 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
970 / out_alpha) as u8;
971 let out_b = ((src[2] as f32 * src_alpha
972 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
973 / out_alpha) as u8;
974 let out_a = (out_alpha * 255.0) as u8;
975 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
976 }
977 }
978}
979
980fn blend_multiply(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
982 let (w1, h1) = base.dimensions();
983 let (w2, h2) = overlay.dimensions();
984 let opacity = opacity.clamp(0.0, 1.0);
985
986 for oy in 0..h2 {
987 for ox in 0..w2 {
988 let bx = x + ox as i32;
989 let by = y + oy as i32;
990 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
991 continue;
992 }
993 let src = overlay.get_pixel(ox, oy);
994 let dst = base.get_pixel(bx as u32, by as u32);
995 let src_alpha = (src[3] as f32 / 255.0) * opacity;
996 if src_alpha < 1e-6 {
997 continue;
998 }
999 let mult_r = (src[0] as u16 * dst[0] as u16 / 255) as u8;
1001 let mult_g = (src[1] as u16 * dst[1] as u16 / 255) as u8;
1002 let mult_b = (src[2] as u16 * dst[2] as u16 / 255) as u8;
1003 let dst_alpha = dst[3] as f32 / 255.0;
1004 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1005 if out_alpha < 1e-6 {
1006 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1007 continue;
1008 }
1009 let out_r = ((mult_r as f32 * src_alpha
1010 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1011 / out_alpha) as u8;
1012 let out_g = ((mult_g as f32 * src_alpha
1013 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1014 / out_alpha) as u8;
1015 let out_b = ((mult_b as f32 * src_alpha
1016 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1017 / out_alpha) as u8;
1018 let out_a = (out_alpha * 255.0) as u8;
1019 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1020 }
1021 }
1022}
1023
1024fn blend_overlay(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
1026 let (w1, h1) = base.dimensions();
1027 let (w2, h2) = overlay.dimensions();
1028 let opacity = opacity.clamp(0.0, 1.0);
1029
1030 for oy in 0..h2 {
1031 for ox in 0..w2 {
1032 let bx = x + ox as i32;
1033 let by = y + oy as i32;
1034 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
1035 continue;
1036 }
1037 let src = overlay.get_pixel(ox, oy);
1038 let dst = base.get_pixel(bx as u32, by as u32);
1039 let src_alpha = (src[3] as f32 / 255.0) * opacity;
1040 if src_alpha < 1e-6 {
1041 continue;
1042 }
1043 let overlay_channel = |s: u8, d: u8| -> u8 {
1045 if d <= 128 {
1046 (2 * s as u16 * d as u16 / 255) as u8
1047 } else {
1048 (255 - (2 * (255 - s) as u16 * (255 - d) as u16 / 255)) as u8
1049 }
1050 };
1051 let ov_r = overlay_channel(src[0], dst[0]);
1052 let ov_g = overlay_channel(src[1], dst[1]);
1053 let ov_b = overlay_channel(src[2], dst[2]);
1054 let dst_alpha = dst[3] as f32 / 255.0;
1055 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1056 if out_alpha < 1e-6 {
1057 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1058 continue;
1059 }
1060 let out_r = ((ov_r as f32 * src_alpha + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1061 / out_alpha) as u8;
1062 let out_g = ((ov_g as f32 * src_alpha + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1063 / out_alpha) as u8;
1064 let out_b = ((ov_b as f32 * src_alpha + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1065 / out_alpha) as u8;
1066 let out_a = (out_alpha * 255.0) as u8;
1067 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1068 }
1069 }
1070}
1071
1072fn blend_screen(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
1074 let (w1, h1) = base.dimensions();
1075 let (w2, h2) = overlay.dimensions();
1076 let opacity = opacity.clamp(0.0, 1.0);
1077
1078 for oy in 0..h2 {
1079 for ox in 0..w2 {
1080 let bx = x + ox as i32;
1081 let by = y + oy as i32;
1082 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
1083 continue;
1084 }
1085 let src = overlay.get_pixel(ox, oy);
1086 let dst = base.get_pixel(bx as u32, by as u32);
1087 let src_alpha = (src[3] as f32 / 255.0) * opacity;
1088 if src_alpha < 1e-6 {
1089 continue;
1090 }
1091 let screen_r = (255 - (255 - src[0]) as u16 * (255 - dst[0]) as u16 / 255) as u8;
1093 let screen_g = (255 - (255 - src[1]) as u16 * (255 - dst[1]) as u16 / 255) as u8;
1094 let screen_b = (255 - (255 - src[2]) as u16 * (255 - dst[2]) as u16 / 255) as u8;
1095 let dst_alpha = dst[3] as f32 / 255.0;
1096 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1097 if out_alpha < 1e-6 {
1098 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1099 continue;
1100 }
1101 let out_r = ((screen_r as f32 * src_alpha
1102 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1103 / out_alpha) as u8;
1104 let out_g = ((screen_g as f32 * src_alpha
1105 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1106 / out_alpha) as u8;
1107 let out_b = ((screen_b as f32 * src_alpha
1108 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1109 / out_alpha) as u8;
1110 let out_a = (out_alpha * 255.0) as u8;
1111 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1112 }
1113 }
1114}
1115
1116async fn load_font(font_path: Option<&Path>) -> Result<FontVec, ImageError> {
1124 match font_path {
1125 Some(path) => {
1126 let data = tokio::fs::read(path)
1127 .await
1128 .map_err(|e| ImageError::FontLoadFailed(format!("{path:?}: {e}")))?;
1129 Ok(FontVec::try_from_vec(data)
1130 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font {path:?}: {e}")))?)
1131 }
1132 None => {
1133 Err(ImageError::FontLoadFailed(
1135 "font_path is required (no default font available)".to_string(),
1136 ))
1137 }
1138 }
1139}
1140
1141pub async fn measure_text(
1157 font_path: &Path,
1158 size: u32,
1159 text: &str,
1160) -> Result<TextMetrics, ImageError> {
1161 let data = tokio::fs::read(font_path)
1162 .await
1163 .map_err(|e| ImageError::FontLoadFailed(format!("{font_path:?}: {e}")))?;
1164 let font = FontVec::try_from_vec(data)
1165 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font: {e}")))?;
1166 Ok(measure_text_with_font(&font, size, text))
1167}
1168
1169fn measure_text_with_font<F: Font>(font: &F, size: u32, text: &str) -> TextMetrics {
1171 let scale = PxScale::from(size as f32);
1172 let scaled = font.as_scaled(scale);
1173 let ascent = scaled.ascent();
1174 let descent = scaled.descent();
1175 let height = (ascent - descent).ceil();
1176
1177 let mut width: f32 = 0.0;
1178 let mut prev_glyph: Option<Glyph> = None;
1179 for ch in text.chars() {
1180 let glyph = scaled.scaled_glyph(ch);
1181 if let Some(prev) = prev_glyph {
1182 width += scaled.kern(prev.id, glyph.id);
1183 }
1184 width += scaled.h_advance(glyph.id);
1185 prev_glyph = Some(glyph);
1186 }
1187
1188 TextMetrics {
1189 width: width.ceil() as i32,
1190 height: height.ceil() as i32,
1191 ascent: ascent.ceil() as i32,
1192 descent: descent.ceil() as i32,
1193 }
1194}
1195
1196#[derive(Debug, Clone, Copy)]
1198pub struct TextMetrics {
1199 pub width: i32,
1201 pub height: i32,
1203 pub ascent: i32,
1205 pub descent: i32,
1207}
1208
1209pub async fn wrap_text(
1253 font_path: &Path,
1254 fontsize: u32,
1255 string: &str,
1256 width: i32,
1257 max_line: Option<usize>,
1258) -> Result<String, ImageError> {
1259 let data = tokio::fs::read(font_path)
1260 .await
1261 .map_err(|e| ImageError::FontLoadFailed(format!("{font_path:?}: {e}")))?;
1262 let font = FontVec::try_from_vec(data)
1263 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font: {e}")))?;
1264 Ok(wrap_text_with_font(
1265 &font, fontsize, string, width, max_line,
1266 ))
1267}
1268
1269fn wrap_text_with_font<F: Font>(
1271 font: &F,
1272 fontsize: u32,
1273 string: &str,
1274 width: i32,
1275 max_line: Option<usize>,
1276) -> String {
1277 let mut content = String::new();
1278 let mut line_count: usize = 0;
1279 for l in string.chars() {
1280 let test = format!("{content} {l}");
1282 let metrics = measure_text_with_font(font, fontsize, &test);
1283 if metrics.width > width && !content.is_empty() {
1285 line_count += 1;
1286 if let Some(ml) = max_line {
1287 if line_count >= ml {
1288 let trimmed: String =
1290 content.chars().take(content.chars().count() - 1).collect();
1291 content = format!("{trimmed}...");
1292 break;
1293 }
1294 }
1295 content.push('\n');
1296 }
1297 content.push(l);
1298 }
1299 content
1300}
1301
1302#[cfg(test)]
1307mod tests {
1308 use super::*;
1309
1310 #[test]
1313 fn test_image_type_as_str() {
1314 assert_eq!(ImageType::Unknown.as_str(), "");
1315 assert_eq!(ImageType::Gif.as_str(), "GIF");
1316 assert_eq!(ImageType::Jpeg.as_str(), "JPEG");
1317 assert_eq!(ImageType::Png.as_str(), "PNG");
1318 assert_eq!(ImageType::Wbmp.as_str(), "WBMP");
1319 }
1320
1321 #[test]
1322 fn test_image_type_from_extension() {
1323 assert_eq!(ImageType::from_extension("gif"), ImageType::Gif);
1324 assert_eq!(ImageType::from_extension("jpg"), ImageType::Jpeg);
1325 assert_eq!(ImageType::from_extension("jpeg"), ImageType::Jpeg);
1326 assert_eq!(ImageType::from_extension("png"), ImageType::Png);
1327 assert_eq!(ImageType::from_extension("wbmp"), ImageType::Wbmp);
1328 assert_eq!(ImageType::from_extension("unknown"), ImageType::Unknown);
1329 }
1330
1331 #[test]
1332 fn test_image_type_default() {
1333 assert_eq!(ImageType::default(), ImageType::Unknown);
1334 }
1335
1336 #[test]
1337 fn test_image_type_from_image_format() {
1338 use image::ImageFormat;
1339 assert_eq!(
1340 ImageType::from_image_format(ImageFormat::Gif),
1341 ImageType::Gif
1342 );
1343 assert_eq!(
1344 ImageType::from_image_format(ImageFormat::Jpeg),
1345 ImageType::Jpeg
1346 );
1347 assert_eq!(
1348 ImageType::from_image_format(ImageFormat::Png),
1349 ImageType::Png
1350 );
1351 assert_eq!(
1352 ImageType::from_image_format(ImageFormat::WebP),
1353 ImageType::Unknown
1354 );
1355 }
1356
1357 #[test]
1358 fn test_image_type_to_image_format() {
1359 assert_eq!(
1360 ImageType::Gif.to_image_format(),
1361 Some(image::ImageFormat::Gif)
1362 );
1363 assert_eq!(
1364 ImageType::Jpeg.to_image_format(),
1365 Some(image::ImageFormat::Jpeg)
1366 );
1367 assert_eq!(
1368 ImageType::Png.to_image_format(),
1369 Some(image::ImageFormat::Png)
1370 );
1371 assert_eq!(ImageType::Wbmp.to_image_format(), None);
1372 assert_eq!(ImageType::Unknown.to_image_format(), None);
1373 }
1374
1375 #[test]
1376 fn test_image_type_from_extension_case_insensitive() {
1377 assert_eq!(ImageType::from_extension("GIF"), ImageType::Gif);
1378 assert_eq!(ImageType::from_extension("PNG"), ImageType::Png);
1379 assert_eq!(ImageType::from_extension("JPG"), ImageType::Jpeg);
1380 }
1381
1382 #[test]
1385 fn test_color_rgb() {
1386 let c = Color::rgb(255, 128, 0);
1387 assert_eq!(c.r, 255);
1388 assert_eq!(c.g, 128);
1389 assert_eq!(c.b, 0);
1390 assert_eq!(c.a, 255); }
1392
1393 #[test]
1394 fn test_color_rgba() {
1395 let c = Color::rgba(255, 128, 0, 128);
1396 assert_eq!(c.r, 255);
1397 assert_eq!(c.g, 128);
1398 assert_eq!(c.b, 0);
1399 assert_eq!(c.a, 128);
1400 }
1401
1402 #[test]
1403 fn test_color_from_hex_rrggbb() {
1404 let c = Color::from_hex("#ff8000").unwrap();
1405 assert_eq!(c.r, 255);
1406 assert_eq!(c.g, 128);
1407 assert_eq!(c.b, 0);
1408 assert_eq!(c.a, 255);
1409 }
1410
1411 #[test]
1412 fn test_color_from_hex_rgb() {
1413 let c = Color::from_hex("#f80").unwrap();
1414 assert_eq!(c.r, 255);
1415 assert_eq!(c.g, 136);
1416 assert_eq!(c.b, 0);
1417 assert_eq!(c.a, 255);
1418 }
1419
1420 #[test]
1421 fn test_color_from_hex_rrggbbaa() {
1422 let c = Color::from_hex("#ff800080").unwrap();
1423 assert_eq!(c.r, 255);
1424 assert_eq!(c.g, 128);
1425 assert_eq!(c.b, 0);
1426 assert_eq!(c.a, 128);
1427 }
1428
1429 #[test]
1430 fn test_color_from_hex_no_hash() {
1431 let c = Color::from_hex("ff8000").unwrap();
1432 assert_eq!(c.r, 255);
1433 assert_eq!(c.g, 128);
1434 assert_eq!(c.b, 0);
1435 }
1436
1437 #[test]
1438 fn test_color_from_hex_invalid() {
1439 assert!(Color::from_hex("#xyz").is_err());
1440 assert!(Color::from_hex("#1").is_err());
1441 assert!(Color::from_hex("12345").is_err());
1442 }
1443
1444 #[test]
1445 fn test_color_to_rgba() {
1446 let c = Color::rgb(1, 2, 3);
1447 assert_eq!(c.to_rgba(), Rgba([1, 2, 3, 255]));
1448 }
1449
1450 #[test]
1451 fn test_color_default() {
1452 let c = Color::default();
1453 assert_eq!(c, Color::rgb(0, 0, 0));
1454 }
1455
1456 #[test]
1459 fn test_position_parse() {
1460 assert_eq!(Position::parse("top-left").unwrap(), Position::TopLeft);
1461 assert_eq!(Position::parse("top-center").unwrap(), Position::TopCenter);
1462 assert_eq!(Position::parse("TOP-RIGHT").unwrap(), Position::TopRight);
1463 assert_eq!(Position::parse("center").unwrap(), Position::Center);
1464 assert_eq!(
1465 Position::parse("bottom-right").unwrap(),
1466 Position::BottomRight
1467 );
1468 }
1469
1470 #[test]
1471 fn test_position_parse_invalid() {
1472 assert!(Position::parse("invalid").is_err());
1473 assert!(Position::parse("").is_err());
1474 }
1475
1476 #[test]
1477 fn test_position_as_str() {
1478 assert_eq!(Position::TopLeft.as_str(), "top-left");
1479 assert_eq!(Position::Center.as_str(), "center");
1480 assert_eq!(Position::BottomRight.as_str(), "bottom-right");
1481 }
1482
1483 #[test]
1484 fn test_position_get_xy_top_left() {
1485 let (x, y) = Position::TopLeft.get_xy(100, 100, 20, 20);
1487 assert_eq!(x, 0);
1488 assert_eq!(y, 0);
1489 }
1490
1491 #[test]
1492 fn test_position_get_xy_center() {
1493 let (x, y) = Position::Center.get_xy(100, 100, 20, 20);
1495 assert_eq!(x, 40);
1496 assert_eq!(y, 40);
1497 }
1498
1499 #[test]
1500 fn test_position_get_xy_bottom_right() {
1501 let (x, y) = Position::BottomRight.get_xy(100, 100, 20, 20);
1503 assert_eq!(x, 80);
1504 assert_eq!(y, 80);
1505 }
1506
1507 #[test]
1508 fn test_position_get_xy_top_center() {
1509 let (x, y) = Position::TopCenter.get_xy(100, 100, 20, 20);
1510 assert_eq!(x, 40); assert_eq!(y, 0);
1512 }
1513
1514 fn create_test_png(path: &Path, w: u32, h: u32, color: Rgba<u8>) {
1517 let img: RgbaImage = ImageBuffer::from_pixel(w, h, color);
1518 img.save(path).unwrap();
1519 }
1520
1521 #[test]
1522 fn test_image_create_blank() {
1523 let img = Image::create_blank(100, 50);
1524 assert_eq!(img.width(), 100);
1525 assert_eq!(img.height(), 50);
1526 assert_eq!(img.image_type(), ImageType::Unknown);
1527 assert!(img.file_path().is_none());
1528 }
1529
1530 #[tokio::test]
1531 async fn test_image_open_png() {
1532 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1533 let path = tmp.path();
1534 create_test_png(path, 80, 60, Rgba([255, 0, 0, 255]));
1535 let img = Image::open(path).await.unwrap();
1536 assert_eq!(img.width(), 80);
1537 assert_eq!(img.height(), 60);
1538 assert_eq!(img.image_type(), ImageType::Png);
1539 assert!(img.file_path().is_some());
1540 }
1541
1542 #[test]
1543 fn test_image_from_dynamic() {
1544 let buf: RgbaImage = ImageBuffer::from_pixel(50, 50, Rgba([0, 255, 0, 255]));
1545 let dyn_img = DynamicImage::ImageRgba8(buf);
1546 let img = Image::from_dynamic(dyn_img, ImageType::Png);
1547 assert_eq!(img.width(), 50);
1548 assert_eq!(img.height(), 50);
1549 assert_eq!(img.image_type(), ImageType::Png);
1550 }
1551
1552 #[test]
1553 fn test_image_to_rgba8() {
1554 let img = Image::create_blank(30, 30);
1555 let rgba = img.to_rgba8();
1556 assert_eq!(rgba.dimensions(), (30, 30));
1557 }
1558
1559 #[test]
1562 fn test_editor_new() {
1563 let _editor = Editor::new();
1564 let _editor2 = Editor;
1565 }
1566
1567 #[tokio::test]
1568 async fn test_editor_open() {
1569 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1570 create_test_png(tmp.path(), 100, 100, Rgba([0, 0, 255, 255]));
1571 let editor = Editor::new();
1572 let img = editor.open(tmp.path()).await.unwrap();
1573 assert_eq!(img.width(), 100);
1574 assert_eq!(img.height(), 100);
1575 }
1576
1577 #[tokio::test]
1578 async fn test_editor_save_png() {
1579 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1580 create_test_png(tmp_in.path(), 50, 50, Rgba([0, 255, 0, 255]));
1581 let tmp_out = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1582 let editor = Editor::new();
1583 let img = editor.open(tmp_in.path()).await.unwrap();
1584 editor
1585 .save(&img, tmp_out.path(), None, None, false, 0o755)
1586 .await
1587 .unwrap();
1588 let reopened = image::open(tmp_out.path()).unwrap();
1590 assert_eq!(reopened.width(), 50);
1591 assert_eq!(reopened.height(), 50);
1592 }
1593
1594 #[tokio::test]
1595 async fn test_editor_save_jpeg_with_quality() {
1596 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1597 create_test_png(tmp_in.path(), 50, 50, Rgba([128, 64, 32, 255]));
1598 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
1599 let editor = Editor::new();
1600 let img = editor.open(tmp_in.path()).await.unwrap();
1601 editor
1602 .save(&img, tmp_out.path(), None, Some(90), false, 0o755)
1603 .await
1604 .unwrap();
1605 let reopened = image::open(tmp_out.path()).unwrap();
1606 assert_eq!(reopened.width(), 50);
1607 assert_eq!(reopened.height(), 50);
1608 }
1609
1610 #[tokio::test]
1613 async fn test_editor_resize_exact() {
1614 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1615 create_test_png(tmp.path(), 100, 100, Rgba([255, 0, 0, 255]));
1616 let editor = Editor::new();
1617 let mut img = editor.open(tmp.path()).await.unwrap();
1618 editor.resize_exact(&mut img, 50, 80);
1619 assert_eq!(img.width(), 50);
1620 assert_eq!(img.height(), 80);
1621 }
1622
1623 #[tokio::test]
1624 async fn test_editor_resize_fit() {
1625 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1626 create_test_png(tmp.path(), 200, 100, Rgba([0, 255, 0, 255]));
1627 let editor = Editor::new();
1628 let mut img = editor.open(tmp.path()).await.unwrap();
1629 editor.resize_fit(&mut img, 100, 100);
1631 assert_eq!(img.width(), 100);
1632 assert_eq!(img.height(), 50);
1633 }
1634
1635 #[tokio::test]
1636 async fn test_editor_resize_fill() {
1637 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1638 create_test_png(tmp.path(), 200, 100, Rgba([0, 0, 255, 255]));
1639 let editor = Editor::new();
1640 let mut img = editor.open(tmp.path()).await.unwrap();
1641 editor.resize_fill(&mut img, 100, 100);
1643 assert_eq!(img.width(), 100);
1644 assert_eq!(img.height(), 100);
1645 }
1646
1647 #[tokio::test]
1648 async fn test_editor_resize_exact_width() {
1649 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1650 create_test_png(tmp.path(), 200, 100, Rgba([255, 255, 0, 255]));
1651 let editor = Editor::new();
1652 let mut img = editor.open(tmp.path()).await.unwrap();
1653 editor.resize_exact_width(&mut img, 50);
1655 assert_eq!(img.width(), 50);
1656 assert_eq!(img.height(), 25);
1657 }
1658
1659 #[tokio::test]
1660 async fn test_editor_resize_exact_height() {
1661 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1662 create_test_png(tmp.path(), 200, 100, Rgba([255, 0, 255, 255]));
1663 let editor = Editor::new();
1664 let mut img = editor.open(tmp.path()).await.unwrap();
1665 editor.resize_exact_height(&mut img, 50);
1667 assert_eq!(img.width(), 100);
1668 assert_eq!(img.height(), 50);
1669 }
1670
1671 #[tokio::test]
1674 async fn test_editor_crop_center() {
1675 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1676 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
1677 let editor = Editor::new();
1678 let mut img = editor.open(tmp.path()).await.unwrap();
1679 editor
1680 .crop(&mut img, 50, 50, Position::Center, 0, 0)
1681 .unwrap();
1682 assert_eq!(img.width(), 50);
1683 assert_eq!(img.height(), 50);
1684 }
1685
1686 #[tokio::test]
1687 async fn test_editor_crop_too_large() {
1688 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1689 create_test_png(tmp.path(), 50, 50, Rgba([0, 0, 0, 255]));
1690 let editor = Editor::new();
1691 let mut img = editor.open(tmp.path()).await.unwrap();
1692 assert!(editor
1693 .crop(&mut img, 100, 100, Position::TopLeft, 0, 0)
1694 .is_err());
1695 }
1696
1697 #[tokio::test]
1698 async fn test_editor_flip_horizontal() {
1699 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1700 create_test_png(tmp.path(), 80, 60, Rgba([255, 128, 64, 255]));
1701 let editor = Editor::new();
1702 let mut img = editor.open(tmp.path()).await.unwrap();
1703 editor.flip(&mut img, FlipMode::Horizontal);
1704 assert_eq!(img.width(), 80);
1705 assert_eq!(img.height(), 60);
1706 }
1707
1708 #[tokio::test]
1709 async fn test_editor_flip_vertical() {
1710 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1711 create_test_png(tmp.path(), 80, 60, Rgba([255, 128, 64, 255]));
1712 let editor = Editor::new();
1713 let mut img = editor.open(tmp.path()).await.unwrap();
1714 editor.flip(&mut img, FlipMode::Vertical);
1715 assert_eq!(img.width(), 80);
1716 assert_eq!(img.height(), 60);
1717 }
1718
1719 #[tokio::test]
1720 async fn test_editor_rotate_90() {
1721 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1722 create_test_png(tmp.path(), 80, 60, Rgba([64, 255, 128, 255]));
1723 let editor = Editor::new();
1724 let mut img = editor.open(tmp.path()).await.unwrap();
1725 editor.rotate(&mut img, 90.0).unwrap();
1726 assert_eq!(img.width(), 60); assert_eq!(img.height(), 80);
1728 }
1729
1730 #[tokio::test]
1731 async fn test_editor_rotate_180() {
1732 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1733 create_test_png(tmp.path(), 80, 60, Rgba([64, 128, 255, 255]));
1734 let editor = Editor::new();
1735 let mut img = editor.open(tmp.path()).await.unwrap();
1736 editor.rotate(&mut img, 180.0).unwrap();
1737 assert_eq!(img.width(), 80);
1738 assert_eq!(img.height(), 60);
1739 }
1740
1741 #[tokio::test]
1742 async fn test_editor_rotate_invalid_angle() {
1743 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1744 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
1745 let editor = Editor::new();
1746 let mut img = editor.open(tmp.path()).await.unwrap();
1747 assert!(editor.rotate(&mut img, 45.0).is_err());
1748 }
1749
1750 #[tokio::test]
1753 async fn test_editor_blend_normal() {
1754 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1755 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1756 create_test_png(tmp1.path(), 100, 100, Rgba([0, 0, 0, 255]));
1757 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1758 let editor = Editor::new();
1759 let mut img1 = editor.open(tmp1.path()).await.unwrap();
1760 let img2 = editor.open(tmp2.path()).await.unwrap();
1761 editor
1762 .blend(
1763 &mut img1,
1764 &img2,
1765 BlendType::Normal,
1766 1.0,
1767 Position::TopLeft,
1768 0,
1769 0,
1770 )
1771 .unwrap();
1772 assert_eq!(img1.width(), 100);
1773 assert_eq!(img1.height(), 100);
1774 let rgba = img1.to_rgba8();
1776 let pixel = rgba.get_pixel(0, 0);
1777 assert_eq!(pixel[0], 255);
1778 assert_eq!(pixel[1], 255);
1779 assert_eq!(pixel[2], 255);
1780 }
1781
1782 #[tokio::test]
1783 async fn test_editor_blend_with_offset() {
1784 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1785 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1786 create_test_png(tmp1.path(), 100, 100, Rgba([0, 0, 0, 255]));
1787 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1788 let editor = Editor::new();
1789 let mut img1 = editor.open(tmp1.path()).await.unwrap();
1790 let img2 = editor.open(tmp2.path()).await.unwrap();
1791 editor
1793 .blend(
1794 &mut img1,
1795 &img2,
1796 BlendType::Normal,
1797 1.0,
1798 Position::TopLeft,
1799 30,
1800 30,
1801 )
1802 .unwrap();
1803 let rgba = img1.to_rgba8();
1804 let p1 = rgba.get_pixel(0, 0);
1806 assert_eq!(p1[0], 0);
1807 let p2 = rgba.get_pixel(50, 50);
1809 assert_eq!(p2[0], 255);
1810 let p3 = rgba.get_pixel(90, 90);
1812 assert_eq!(p3[0], 0);
1813 }
1814
1815 #[tokio::test]
1816 async fn test_editor_blend_opacity_half() {
1817 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1818 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1819 create_test_png(tmp1.path(), 50, 50, Rgba([0, 0, 0, 255]));
1820 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1821 let editor = Editor::new();
1822 let mut img1 = editor.open(tmp1.path()).await.unwrap();
1823 let img2 = editor.open(tmp2.path()).await.unwrap();
1824 editor
1826 .blend(
1827 &mut img1,
1828 &img2,
1829 BlendType::Normal,
1830 0.5,
1831 Position::TopLeft,
1832 0,
1833 0,
1834 )
1835 .unwrap();
1836 let rgba = img1.to_rgba8();
1837 let pixel = rgba.get_pixel(0, 0);
1838 assert!(
1840 (120..=136).contains(&pixel[0]),
1841 "expected ~128, got {}",
1842 pixel[0]
1843 );
1844 }
1845
1846 #[test]
1847 fn test_blend_type_parse() {
1848 assert_eq!(BlendType::parse("normal").unwrap(), BlendType::Normal);
1849 assert_eq!(BlendType::parse("MULTIPLY").unwrap(), BlendType::Multiply);
1850 assert_eq!(BlendType::parse("overlay").unwrap(), BlendType::Overlay);
1851 assert_eq!(BlendType::parse("screen").unwrap(), BlendType::Screen);
1852 assert!(BlendType::parse("invalid").is_err());
1853 }
1854
1855 #[test]
1856 fn test_flip_mode_parse() {
1857 assert_eq!(FlipMode::parse("h").unwrap(), FlipMode::Horizontal);
1858 assert_eq!(FlipMode::parse("V").unwrap(), FlipMode::Vertical);
1859 assert!(FlipMode::parse("x").is_err());
1860 }
1861
1862 #[test]
1865 fn test_editor_fill() {
1866 let img = Image::create_blank(50, 50);
1867 let editor = Editor::new();
1868 let mut img = img;
1869 editor.fill(&mut img, Color::rgb(255, 0, 0));
1870 let rgba = img.to_rgba8();
1871 let pixel = rgba.get_pixel(0, 0);
1872 assert_eq!(pixel[0], 255);
1873 assert_eq!(pixel[1], 0);
1874 assert_eq!(pixel[2], 0);
1875 }
1876
1877 #[test]
1881 fn test_r5_24_image_type_constants() {
1882 assert_eq!(ImageType::Unknown.as_str(), ""); assert_eq!(ImageType::Gif.as_str(), "GIF"); assert_eq!(ImageType::Jpeg.as_str(), "JPEG"); assert_eq!(ImageType::Png.as_str(), "PNG"); assert_eq!(ImageType::Wbmp.as_str(), "WBMP"); }
1889
1890 #[test]
1892 fn test_r5_25_color_hex_parsing() {
1893 let c1 = Color::from_hex("#333333").unwrap();
1895 assert_eq!((c1.r, c1.g, c1.b), (0x33, 0x33, 0x33));
1896 let c2 = Color::from_hex("#ff4444").unwrap();
1898 assert_eq!((c2.r, c2.g, c2.b), (0xff, 0x44, 0x44));
1899 let c3 = Color::from_hex("#f00").unwrap();
1901 assert_eq!((c3.r, c3.g, c3.b), (0xff, 0x00, 0x00));
1902 }
1903
1904 #[test]
1906 fn test_r5_26_position_get_xy_all_nine() {
1907 let w1 = 100u32;
1908 let h1 = 100u32;
1909 let w2 = 20u32;
1910 let h2 = 20u32;
1911 let cases = [
1913 (Position::TopLeft, 0, 0),
1914 (Position::TopCenter, 40, 0),
1915 (Position::TopRight, 80, 0),
1916 (Position::CenterLeft, 0, 40),
1917 (Position::Center, 40, 40),
1918 (Position::CenterRight, 80, 40),
1919 (Position::BottomLeft, 0, 80),
1920 (Position::BottomCenter, 40, 80),
1921 (Position::BottomRight, 80, 80),
1922 ];
1923 for (pos, ex, ey) in cases {
1924 let (x, y) = pos.get_xy(w1, h1, w2, h2);
1925 assert_eq!(x, ex, "Position {:?} x mismatch", pos);
1926 assert_eq!(y, ey, "Position {:?} y mismatch", pos);
1927 }
1928 }
1929
1930 #[tokio::test]
1932 async fn test_r5_27_image_open_detects_type() {
1933 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1934 create_test_png(tmp.path(), 80, 60, Rgba([255, 255, 255, 255]));
1935 let img = Image::open(tmp.path()).await.unwrap();
1936 assert_eq!(img.image_type(), ImageType::Png);
1937 assert_eq!(img.width(), 80);
1938 assert_eq!(img.height(), 60);
1939 }
1940
1941 #[tokio::test]
1943 async fn test_r5_28_resize_exact_forces_dimensions() {
1944 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1945 create_test_png(tmp.path(), 200, 100, Rgba([255, 0, 0, 255]));
1946 let editor = Editor::new();
1947 let mut img = editor.open(tmp.path()).await.unwrap();
1948 editor.resize_exact(&mut img, 50, 50);
1950 assert_eq!(img.width(), 50);
1951 assert_eq!(img.height(), 50);
1952 }
1953
1954 #[tokio::test]
1956 async fn test_r5_29_blend_normal_with_offset_and_opacity() {
1957 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1958 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1959 create_test_png(tmp1.path(), 200, 200, Rgba([0, 0, 0, 255]));
1960 create_test_png(tmp2.path(), 100, 100, Rgba([255, 255, 255, 255]));
1961 let editor = Editor::new();
1962 let mut img1 = editor.open(tmp1.path()).await.unwrap();
1963 let img2 = editor.open(tmp2.path()).await.unwrap();
1964 editor
1966 .blend(
1967 &mut img1,
1968 &img2,
1969 BlendType::Normal,
1970 1.0,
1971 Position::TopLeft,
1972 30,
1973 30,
1974 )
1975 .unwrap();
1976 let rgba = img1.to_rgba8();
1978 assert_eq!(rgba.get_pixel(0, 0)[0], 0);
1980 assert_eq!(rgba.get_pixel(50, 50)[0], 255);
1982 assert_eq!(rgba.get_pixel(129, 129)[0], 255);
1984 assert_eq!(rgba.get_pixel(130, 130)[0], 0);
1986 assert_eq!(rgba.get_pixel(150, 150)[0], 0);
1988 }
1989
1990 #[tokio::test]
1992 async fn test_r5_30_text_y_baseline_offset() {
1993 let mut img = Image::create_blank(200, 100);
1998 let editor = Editor::new();
1999 let result = editor
2001 .text(&mut img, "test", 30, 10, 50, Color::rgb(0, 0, 0), None)
2002 .await;
2003 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2004 }
2005
2006 #[tokio::test]
2008 async fn test_r5_31_save_infers_type_from_extension() {
2009 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2010 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 128, 64, 255]));
2011
2012 let tmp_png = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2014 let editor = Editor::new();
2015 let img = editor.open(tmp_in.path()).await.unwrap();
2016 editor
2017 .save(&img, tmp_png.path(), None, None, false, 0o755)
2018 .await
2019 .unwrap();
2020 assert!(tmp_png.path().exists());
2021
2022 let tmp_jpg = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2024 editor
2025 .save(&img, tmp_jpg.path(), None, None, false, 0o755)
2026 .await
2027 .unwrap();
2028 assert!(tmp_jpg.path().exists());
2029 }
2030
2031 #[tokio::test]
2033 async fn test_r5_32_wrap_text_signature_alignment() {
2034 let result = wrap_text(Path::new("/nonexistent.ttf"), 30, "hello", 680, Some(2)).await;
2038 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2039 }
2040
2041 #[test]
2043 fn test_r5_32_wrap_text_with_font_logic() {
2044 let font_path = Path::new(
2047 "e:/vue/test/鲜视达/server/vendor/kosinix/grafika/src/Grafika/fonts/st-heiti-light.ttc",
2048 );
2049 if !font_path.exists() {
2050 eprintln!(
2052 "Skipping test_r5_32_wrap_text_with_font_logic: font not found at {font_path:?}"
2053 );
2054 return;
2055 }
2056 let data = std::fs::read(font_path).unwrap();
2057 let font = FontVec::try_from_vec(data).unwrap();
2058 let result = wrap_text_with_font(&font, 30, "hello", 680, Some(2));
2060 assert_eq!(result, "hello");
2061 let long_text = "这是一个非常长的商品名称用于测试自动换行功能应该被截断并添加省略号";
2063 let result = wrap_text_with_font(&font, 30, long_text, 100, Some(2));
2064 assert!(
2065 result.ends_with("..."),
2066 "result should end with ..., got: {result}"
2067 );
2068 assert!(
2069 result.contains('\n'),
2070 "result should contain newline, got: {result}"
2071 );
2072 }
2073
2074 #[tokio::test]
2077 async fn test_measure_text_nonexistent_font() {
2078 let result = measure_text(Path::new("/nonexistent.ttf"), 30, "hello").await;
2079 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2080 }
2081
2082 #[test]
2083 fn test_text_metrics_debug() {
2084 let m = TextMetrics {
2085 width: 100,
2086 height: 30,
2087 ascent: 25,
2088 descent: -5,
2089 };
2090 assert_eq!(m.width, 100);
2091 assert_eq!(m.height, 30);
2092 }
2093
2094 #[test]
2097 fn test_color_from_hex_rgba_short() {
2098 let c = Color::from_hex("#f80f").unwrap();
2099 assert_eq!(c.r, 255);
2100 assert_eq!(c.g, 136);
2101 assert_eq!(c.b, 0);
2102 assert_eq!(c.a, 255);
2103 }
2104
2105 #[test]
2106 fn test_color_from_hex_with_whitespace() {
2107 let c = Color::from_hex(" #ff8000 ").unwrap();
2108 assert_eq!(c.r, 255);
2109 assert_eq!(c.g, 128);
2110 assert_eq!(c.b, 0);
2111 }
2112
2113 #[test]
2114 fn test_color_from_hex_empty() {
2115 assert!(Color::from_hex("#").is_err());
2116 assert!(Color::from_hex("").is_err());
2117 }
2118
2119 #[test]
2120 fn test_color_from_hex_invalid_chars() {
2121 assert!(Color::from_hex("#gggggg").is_err());
2122 assert!(Color::from_hex("#zz").is_err());
2123 }
2124
2125 #[test]
2126 fn test_color_copy_and_eq() {
2127 let c1 = Color::rgb(1, 2, 3);
2128 let c2 = c1;
2129 assert_eq!(c1, c2);
2130 }
2131
2132 #[test]
2135 fn test_image_type_from_image_format_other() {
2136 use image::ImageFormat;
2137 assert_eq!(
2138 ImageType::from_image_format(ImageFormat::Bmp),
2139 ImageType::Unknown
2140 );
2141 assert_eq!(
2142 ImageType::from_image_format(ImageFormat::Tiff),
2143 ImageType::Unknown
2144 );
2145 }
2146
2147 #[test]
2148 fn test_image_type_from_extension_empty() {
2149 assert_eq!(ImageType::from_extension(""), ImageType::Unknown);
2150 }
2151
2152 #[test]
2155 fn test_position_parse_all_nine() {
2156 assert_eq!(Position::parse("top-left").unwrap(), Position::TopLeft);
2157 assert_eq!(Position::parse("top-center").unwrap(), Position::TopCenter);
2158 assert_eq!(Position::parse("top-right").unwrap(), Position::TopRight);
2159 assert_eq!(
2160 Position::parse("center-left").unwrap(),
2161 Position::CenterLeft
2162 );
2163 assert_eq!(Position::parse("center").unwrap(), Position::Center);
2164 assert_eq!(
2165 Position::parse("center-right").unwrap(),
2166 Position::CenterRight
2167 );
2168 assert_eq!(
2169 Position::parse("bottom-left").unwrap(),
2170 Position::BottomLeft
2171 );
2172 assert_eq!(
2173 Position::parse("bottom-center").unwrap(),
2174 Position::BottomCenter
2175 );
2176 assert_eq!(
2177 Position::parse("bottom-right").unwrap(),
2178 Position::BottomRight
2179 );
2180 }
2181
2182 #[test]
2183 fn test_position_as_str_all_nine() {
2184 assert_eq!(Position::TopLeft.as_str(), "top-left");
2185 assert_eq!(Position::TopCenter.as_str(), "top-center");
2186 assert_eq!(Position::TopRight.as_str(), "top-right");
2187 assert_eq!(Position::CenterLeft.as_str(), "center-left");
2188 assert_eq!(Position::Center.as_str(), "center");
2189 assert_eq!(Position::CenterRight.as_str(), "center-right");
2190 assert_eq!(Position::BottomLeft.as_str(), "bottom-left");
2191 assert_eq!(Position::BottomCenter.as_str(), "bottom-center");
2192 assert_eq!(Position::BottomRight.as_str(), "bottom-right");
2193 }
2194
2195 #[test]
2196 fn test_position_get_xy_unequal_dimensions() {
2197 let (x, y) = Position::Center.get_xy(200, 100, 40, 30);
2198 assert_eq!(x, 80);
2199 assert_eq!(y, 35);
2200 }
2201
2202 #[test]
2205 fn test_image_from_rgba8() {
2206 let buf: RgbaImage = ImageBuffer::from_pixel(40, 30, Rgba([10, 20, 30, 255]));
2207 let img = Image::from_rgba8(buf, ImageType::Png);
2208 assert_eq!(img.width(), 40);
2209 assert_eq!(img.height(), 30);
2210 assert_eq!(img.image_type(), ImageType::Png);
2211 assert!(img.file_path().is_none());
2212 }
2213
2214 #[test]
2215 fn test_image_as_dynamic() {
2216 let img = Image::create_blank(50, 50);
2217 let dyn_ref = img.as_dynamic();
2218 assert_eq!(dyn_ref.width(), 50);
2219 assert_eq!(dyn_ref.height(), 50);
2220 }
2221
2222 #[test]
2223 fn test_image_as_dynamic_mut() {
2224 let mut img = Image::create_blank(50, 50);
2225 let dyn_mut = img.as_dynamic_mut();
2226 assert_eq!(dyn_mut.width(), 50);
2227 assert_eq!(dyn_mut.height(), 50);
2228 }
2229
2230 #[tokio::test]
2231 async fn test_image_open_nonexistent() {
2232 let result = Image::open(Path::new("/nonexistent/file.png")).await;
2233 assert!(result.is_err());
2234 }
2235
2236 #[tokio::test]
2237 async fn test_image_open_unknown_extension_fails() {
2238 let tmp_png = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2241 create_test_png(tmp_png.path(), 60, 40, Rgba([255, 0, 0, 255]));
2242 let bin_path = tmp_png.path().with_extension("bin");
2243 std::fs::rename(tmp_png.path(), &bin_path).unwrap();
2244 let result = Image::open(&bin_path).await;
2245 assert!(result.is_err());
2246 }
2247
2248 #[test]
2251 fn test_editor_default() {
2252 let _editor = Editor;
2253 }
2254
2255 #[tokio::test]
2256 async fn test_editor_rotate_0() {
2257 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2258 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2259 let editor = Editor::new();
2260 let mut img = editor.open(tmp.path()).await.unwrap();
2261 editor.rotate(&mut img, 0.0).unwrap();
2262 assert_eq!(img.width(), 80);
2263 assert_eq!(img.height(), 60);
2264 }
2265
2266 #[tokio::test]
2267 async fn test_editor_rotate_270() {
2268 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2269 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2270 let editor = Editor::new();
2271 let mut img = editor.open(tmp.path()).await.unwrap();
2272 editor.rotate(&mut img, 270.0).unwrap();
2273 assert_eq!(img.width(), 60);
2274 assert_eq!(img.height(), 80);
2275 }
2276
2277 #[tokio::test]
2278 async fn test_editor_rotate_negative_90() {
2279 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2280 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2281 let editor = Editor::new();
2282 let mut img = editor.open(tmp.path()).await.unwrap();
2283 editor.rotate(&mut img, -90.0).unwrap();
2284 assert_eq!(img.width(), 60);
2285 assert_eq!(img.height(), 80);
2286 }
2287
2288 #[tokio::test]
2289 async fn test_editor_rotate_negative_180() {
2290 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2291 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2292 let editor = Editor::new();
2293 let mut img = editor.open(tmp.path()).await.unwrap();
2294 editor.rotate(&mut img, -180.0).unwrap();
2295 assert_eq!(img.width(), 80);
2296 assert_eq!(img.height(), 60);
2297 }
2298
2299 #[tokio::test]
2300 async fn test_editor_rotate_360_normalized_to_0() {
2301 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2302 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2303 let editor = Editor::new();
2304 let mut img = editor.open(tmp.path()).await.unwrap();
2305 editor.rotate(&mut img, 360.0).unwrap();
2306 assert_eq!(img.width(), 80);
2307 assert_eq!(img.height(), 60);
2308 }
2309
2310 #[tokio::test]
2311 async fn test_editor_crop_with_positive_offset() {
2312 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2313 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2314 let editor = Editor::new();
2315 let mut img = editor.open(tmp.path()).await.unwrap();
2316 editor
2317 .crop(&mut img, 50, 50, Position::Center, 10, 10)
2318 .unwrap();
2319 assert_eq!(img.width(), 50);
2320 assert_eq!(img.height(), 50);
2321 }
2322
2323 #[tokio::test]
2324 async fn test_editor_crop_with_negative_offset_clamped() {
2325 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2326 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2327 let editor = Editor::new();
2328 let mut img = editor.open(tmp.path()).await.unwrap();
2329 editor
2330 .crop(&mut img, 50, 50, Position::TopLeft, -100, -100)
2331 .unwrap();
2332 assert_eq!(img.width(), 50);
2333 assert_eq!(img.height(), 50);
2334 }
2335
2336 #[tokio::test]
2337 async fn test_editor_crop_top_left() {
2338 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2339 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2340 let editor = Editor::new();
2341 let mut img = editor.open(tmp.path()).await.unwrap();
2342 editor
2343 .crop(&mut img, 30, 30, Position::TopLeft, 0, 0)
2344 .unwrap();
2345 assert_eq!(img.width(), 30);
2346 assert_eq!(img.height(), 30);
2347 }
2348
2349 #[tokio::test]
2350 async fn test_editor_crop_bottom_right() {
2351 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2352 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2353 let editor = Editor::new();
2354 let mut img = editor.open(tmp.path()).await.unwrap();
2355 editor
2356 .crop(&mut img, 30, 30, Position::BottomRight, 0, 0)
2357 .unwrap();
2358 assert_eq!(img.width(), 30);
2359 assert_eq!(img.height(), 30);
2360 }
2361
2362 #[test]
2365 fn test_editor_blend_multiply() {
2366 let base = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2367 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2368 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2369 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2370 let editor = Editor::new();
2371 editor
2372 .blend(
2373 &mut img1,
2374 &img2,
2375 BlendType::Multiply,
2376 1.0,
2377 Position::TopLeft,
2378 0,
2379 0,
2380 )
2381 .unwrap();
2382 let rgba = img1.to_rgba8();
2383 let pixel = rgba.get_pixel(0, 0);
2384 assert!(
2385 (60..=68).contains(&pixel[0]),
2386 "expected ~64, got {}",
2387 pixel[0]
2388 );
2389 }
2390
2391 #[test]
2392 fn test_editor_blend_overlay_dark() {
2393 let base = ImageBuffer::from_pixel(50, 50, Rgba([64, 64, 64, 255]));
2394 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2395 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2396 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2397 let editor = Editor::new();
2398 editor
2399 .blend(
2400 &mut img1,
2401 &img2,
2402 BlendType::Overlay,
2403 1.0,
2404 Position::TopLeft,
2405 0,
2406 0,
2407 )
2408 .unwrap();
2409 let rgba = img1.to_rgba8();
2410 let pixel = rgba.get_pixel(0, 0);
2411 assert!(
2412 (60..=68).contains(&pixel[0]),
2413 "expected ~64, got {}",
2414 pixel[0]
2415 );
2416 }
2417
2418 #[test]
2419 fn test_editor_blend_overlay_light() {
2420 let base = ImageBuffer::from_pixel(50, 50, Rgba([200, 200, 200, 255]));
2421 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2422 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([200, 200, 200, 255]));
2423 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2424 let editor = Editor::new();
2425 editor
2426 .blend(
2427 &mut img1,
2428 &img2,
2429 BlendType::Overlay,
2430 1.0,
2431 Position::TopLeft,
2432 0,
2433 0,
2434 )
2435 .unwrap();
2436 let rgba = img1.to_rgba8();
2437 let pixel = rgba.get_pixel(0, 0);
2438 assert!(
2439 (225..=235).contains(&pixel[0]),
2440 "expected ~231, got {}",
2441 pixel[0]
2442 );
2443 }
2444
2445 #[test]
2446 fn test_editor_blend_screen() {
2447 let base = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2448 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2449 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2450 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2451 let editor = Editor::new();
2452 editor
2453 .blend(
2454 &mut img1,
2455 &img2,
2456 BlendType::Screen,
2457 1.0,
2458 Position::TopLeft,
2459 0,
2460 0,
2461 )
2462 .unwrap();
2463 let rgba = img1.to_rgba8();
2464 let pixel = rgba.get_pixel(0, 0);
2465 assert_eq!(pixel[0], 0);
2466 }
2467
2468 #[test]
2469 fn test_editor_blend_with_negative_offset() {
2470 let base = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2471 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2472 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([255, 255, 255, 255]));
2473 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2474 let editor = Editor::new();
2475 editor
2476 .blend(
2477 &mut img1,
2478 &img2,
2479 BlendType::Normal,
2480 1.0,
2481 Position::TopLeft,
2482 -25,
2483 -25,
2484 )
2485 .unwrap();
2486 let rgba = img1.to_rgba8();
2487 assert_eq!(rgba.get_pixel(0, 0)[0], 255);
2488 assert_eq!(rgba.get_pixel(24, 24)[0], 255);
2489 assert_eq!(rgba.get_pixel(25, 25)[0], 0);
2490 }
2491
2492 #[test]
2493 fn test_editor_blend_transparent_overlay() {
2494 let base = ImageBuffer::from_pixel(50, 50, Rgba([100, 100, 100, 255]));
2495 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2496 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([255, 0, 0, 0]));
2497 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2498 let editor = Editor::new();
2499 editor
2500 .blend(
2501 &mut img1,
2502 &img2,
2503 BlendType::Normal,
2504 1.0,
2505 Position::TopLeft,
2506 0,
2507 0,
2508 )
2509 .unwrap();
2510 let rgba = img1.to_rgba8();
2511 let pixel = rgba.get_pixel(0, 0);
2512 assert_eq!(pixel[0], 100);
2513 }
2514
2515 #[tokio::test]
2518 async fn test_editor_save_gif() {
2519 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2520 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2521 let tmp_out = tempfile::Builder::new().suffix(".gif").tempfile().unwrap();
2522 let editor = Editor::new();
2523 let img = editor.open(tmp_in.path()).await.unwrap();
2524 editor
2525 .save(&img, tmp_out.path(), None, None, false, 0o755)
2526 .await
2527 .unwrap();
2528 assert!(tmp_out.path().exists());
2529 let reopened = image::open(tmp_out.path()).unwrap();
2530 assert_eq!(reopened.width(), 30);
2531 }
2532
2533 #[tokio::test]
2534 async fn test_editor_save_wbmp_error() {
2535 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2536 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2537 let tmp_out = tempfile::Builder::new().suffix(".wbmp").tempfile().unwrap();
2538 let editor = Editor::new();
2539 let img = editor.open(tmp_in.path()).await.unwrap();
2540 let result = editor
2541 .save(&img, tmp_out.path(), None, None, false, 0o755)
2542 .await;
2543 assert!(result.is_err());
2544 }
2545
2546 #[tokio::test]
2547 async fn test_editor_save_unknown_type_error() {
2548 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2549 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2550 let tmp_out = tempfile::Builder::new().suffix(".bin").tempfile().unwrap();
2551 let editor = Editor::new();
2552 let img = editor.open(tmp_in.path()).await.unwrap();
2553 let result = editor
2554 .save(&img, tmp_out.path(), None, None, false, 0o755)
2555 .await;
2556 assert!(result.is_err());
2557 }
2558
2559 #[tokio::test]
2560 async fn test_editor_save_with_explicit_png_type() {
2561 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2562 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2563 let tmp_out = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2564 let editor = Editor::new();
2565 let img = editor.open(tmp_in.path()).await.unwrap();
2566 editor
2567 .save(
2568 &img,
2569 tmp_out.path(),
2570 Some(ImageType::Png),
2571 None,
2572 false,
2573 0o755,
2574 )
2575 .await
2576 .unwrap();
2577 assert!(tmp_out.path().exists());
2578 }
2579
2580 #[tokio::test]
2581 async fn test_editor_save_jpeg_explicit_type() {
2582 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2583 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 255]));
2584 let tmp_out = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2585 let editor = Editor::new();
2586 let img = editor.open(tmp_in.path()).await.unwrap();
2587 editor
2588 .save(
2589 &img,
2590 tmp_out.path(),
2591 Some(ImageType::Jpeg),
2592 Some(80),
2593 false,
2594 0o755,
2595 )
2596 .await
2597 .unwrap();
2598 assert!(tmp_out.path().exists());
2599 }
2600
2601 #[tokio::test]
2602 async fn test_editor_save_quality_clamping_high() {
2603 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2604 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 255]));
2605 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2606 let editor = Editor::new();
2607 let img = editor.open(tmp_in.path()).await.unwrap();
2608 editor
2609 .save(&img, tmp_out.path(), None, Some(200), false, 0o755)
2610 .await
2611 .unwrap();
2612 assert!(tmp_out.path().exists());
2613 }
2614
2615 #[tokio::test]
2616 async fn test_editor_save_quality_clamping_zero() {
2617 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2618 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 255]));
2619 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2620 let editor = Editor::new();
2621 let img = editor.open(tmp_in.path()).await.unwrap();
2622 editor
2623 .save(&img, tmp_out.path(), None, Some(0), false, 0o755)
2624 .await
2625 .unwrap();
2626 assert!(tmp_out.path().exists());
2627 }
2628
2629 #[tokio::test]
2630 async fn test_editor_save_creates_parent_dir() {
2631 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2632 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2633 let tmp_dir = tempfile::tempdir().unwrap();
2634 let output_path = tmp_dir.path().join("subdir").join("output.png");
2635 assert!(!output_path.parent().unwrap().exists());
2636 let editor = Editor::new();
2637 let img = editor.open(tmp_in.path()).await.unwrap();
2638 editor
2639 .save(&img, &output_path, None, None, false, 0o755)
2640 .await
2641 .unwrap();
2642 assert!(output_path.exists());
2643 }
2644
2645 #[tokio::test]
2648 async fn test_load_font_invalid_data() {
2649 let tmp = tempfile::Builder::new().suffix(".ttf").tempfile().unwrap();
2650 std::fs::write(tmp.path(), b"this is not a font").unwrap();
2651 let mut img = Image::create_blank(100, 50);
2652 let editor = Editor::new();
2653 let result = editor
2654 .text(
2655 &mut img,
2656 "test",
2657 20,
2658 10,
2659 30,
2660 Color::rgb(0, 0, 0),
2661 Some(tmp.path()),
2662 )
2663 .await;
2664 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2665 }
2666
2667 #[tokio::test]
2668 async fn test_measure_text_invalid_font() {
2669 let tmp = tempfile::Builder::new().suffix(".ttf").tempfile().unwrap();
2670 std::fs::write(tmp.path(), b"invalid font data").unwrap();
2671 let result = measure_text(tmp.path(), 30, "hello").await;
2672 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2673 }
2674
2675 #[tokio::test]
2676 async fn test_wrap_text_nonexistent_font() {
2677 let result = wrap_text(Path::new("/nonexistent.ttf"), 30, "hello", 680, None).await;
2678 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2679 }
2680
2681 #[tokio::test]
2682 async fn test_editor_text_with_font() {
2683 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2684 if !font_path.exists() {
2685 eprintln!("Skipping test_editor_text_with_font: font not found");
2686 return;
2687 }
2688 let mut img = Image::create_blank(200, 100);
2689 let editor = Editor::new();
2690 let result = editor
2691 .text(
2692 &mut img,
2693 "hello",
2694 30,
2695 10,
2696 50,
2697 Color::rgb(255, 0, 0),
2698 Some(font_path),
2699 )
2700 .await;
2701 assert!(result.is_ok());
2702 assert_eq!(img.width(), 200);
2703 assert_eq!(img.height(), 100);
2704 }
2705
2706 #[tokio::test]
2707 async fn test_measure_text_with_font() {
2708 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2709 if !font_path.exists() {
2710 eprintln!("Skipping test_measure_text_with_font: font not found");
2711 return;
2712 }
2713 let result = measure_text(font_path, 30, "hello").await.unwrap();
2714 assert!(result.width > 0);
2715 assert!(result.height > 0);
2716 }
2717
2718 #[test]
2719 fn test_wrap_text_with_font_no_max_line() {
2720 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2721 if !font_path.exists() {
2722 eprintln!("Skipping test_wrap_text_with_font_no_max_line: font not found");
2723 return;
2724 }
2725 let data = std::fs::read(font_path).unwrap();
2726 let font = FontVec::try_from_vec(data).unwrap();
2727 let long_text = "this is a very long text that should wrap";
2728 let result = wrap_text_with_font(&font, 30, long_text, 100, None);
2729 assert!(result.contains('\n'), "should contain newline: {result}");
2730 assert!(
2731 !result.ends_with("..."),
2732 "should not end with ... when no max_line"
2733 );
2734 }
2735}