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 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 fn open<P: AsRef<Path>>(&self, path: P) -> Result<Image, ImageError> {
542 Image::open(path)
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 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)?;
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 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 std::fs::create_dir_all(parent)?;
821 #[cfg(unix)]
822 {
823 use std::os::unix::fs::PermissionsExt;
824 let _ = std::fs::set_permissions(
825 parent,
826 std::fs::Permissions::from_mode(permission),
827 );
828 }
829 }
830 }
831
832 match save_type {
834 ImageType::Png => {
835 image.as_dynamic().save(file)?;
836 }
837 ImageType::Jpeg => {
838 let q = quality.unwrap_or(75);
840 let q = q.clamp(1, 100);
841 let rgba = image.to_rgba8();
842 let rgb = image::DynamicImage::ImageRgba8(rgba).to_rgb8();
843 let mut file = std::fs::File::create(file)?;
844 let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut file, q);
845 encoder.encode_image(&image::DynamicImage::ImageRgb8(rgb))?;
846 }
847 ImageType::Gif => {
848 image.as_dynamic().save(file)?;
849 }
850 ImageType::Wbmp => {
851 return Err(ImageError::UnsupportedType(
852 "WBMP encoding not supported by image crate".to_string(),
853 ));
854 }
855 ImageType::Unknown => {
856 return Err(ImageError::UnsupportedType(format!(
857 "Cannot determine save type for file: {file:?}"
858 )));
859 }
860 }
861 Ok(())
862 }
863}
864
865impl Default for Editor {
866 fn default() -> Self {
867 Self::new()
868 }
869}
870
871#[derive(Debug, Clone, Copy, PartialEq, Eq)]
877pub enum BlendType {
878 Normal,
880 Multiply,
882 Overlay,
884 Screen,
886}
887
888impl BlendType {
889 pub fn parse(s: &str) -> Result<Self, ImageError> {
891 match s.to_lowercase().as_str() {
892 "normal" => Ok(Self::Normal),
893 "multiply" => Ok(Self::Multiply),
894 "overlay" => Ok(Self::Overlay),
895 "screen" => Ok(Self::Screen),
896 _ => Err(ImageError::InvalidArgument(format!(
897 "Unknown blend type: {s}"
898 ))),
899 }
900 }
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
909pub enum FlipMode {
910 Horizontal,
912 Vertical,
914}
915
916impl FlipMode {
917 pub fn parse(s: &str) -> Result<Self, ImageError> {
919 match s.to_lowercase().as_str() {
920 "h" => Ok(Self::Horizontal),
921 "v" => Ok(Self::Vertical),
922 _ => Err(ImageError::InvalidArgument(format!(
923 "Unknown flip mode: {s}"
924 ))),
925 }
926 }
927}
928
929fn blend_normal(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
937 let (w1, h1) = base.dimensions();
938 let (w2, h2) = overlay.dimensions();
939 let opacity = opacity.clamp(0.0, 1.0);
940
941 for oy in 0..h2 {
942 for ox in 0..w2 {
943 let bx = x + ox as i32;
944 let by = y + oy as i32;
945 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
946 continue;
947 }
948 let src = overlay.get_pixel(ox, oy);
949 let dst = base.get_pixel(bx as u32, by as u32);
950 let src_alpha = (src[3] as f32 / 255.0) * opacity;
952 if src_alpha < 1e-6 {
953 continue;
954 }
955 let dst_alpha = dst[3] as f32 / 255.0;
956 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
958 if out_alpha < 1e-6 {
959 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
960 continue;
961 }
962 let out_r = ((src[0] as f32 * src_alpha
964 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
965 / out_alpha) as u8;
966 let out_g = ((src[1] as f32 * src_alpha
967 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
968 / out_alpha) as u8;
969 let out_b = ((src[2] as f32 * src_alpha
970 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
971 / out_alpha) as u8;
972 let out_a = (out_alpha * 255.0) as u8;
973 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
974 }
975 }
976}
977
978fn blend_multiply(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
980 let (w1, h1) = base.dimensions();
981 let (w2, h2) = overlay.dimensions();
982 let opacity = opacity.clamp(0.0, 1.0);
983
984 for oy in 0..h2 {
985 for ox in 0..w2 {
986 let bx = x + ox as i32;
987 let by = y + oy as i32;
988 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
989 continue;
990 }
991 let src = overlay.get_pixel(ox, oy);
992 let dst = base.get_pixel(bx as u32, by as u32);
993 let src_alpha = (src[3] as f32 / 255.0) * opacity;
994 if src_alpha < 1e-6 {
995 continue;
996 }
997 let mult_r = (src[0] as u16 * dst[0] as u16 / 255) as u8;
999 let mult_g = (src[1] as u16 * dst[1] as u16 / 255) as u8;
1000 let mult_b = (src[2] as u16 * dst[2] as u16 / 255) as u8;
1001 let dst_alpha = dst[3] as f32 / 255.0;
1002 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1003 if out_alpha < 1e-6 {
1004 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1005 continue;
1006 }
1007 let out_r = ((mult_r as f32 * src_alpha
1008 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1009 / out_alpha) as u8;
1010 let out_g = ((mult_g as f32 * src_alpha
1011 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1012 / out_alpha) as u8;
1013 let out_b = ((mult_b as f32 * src_alpha
1014 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1015 / out_alpha) as u8;
1016 let out_a = (out_alpha * 255.0) as u8;
1017 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1018 }
1019 }
1020}
1021
1022fn blend_overlay(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
1024 let (w1, h1) = base.dimensions();
1025 let (w2, h2) = overlay.dimensions();
1026 let opacity = opacity.clamp(0.0, 1.0);
1027
1028 for oy in 0..h2 {
1029 for ox in 0..w2 {
1030 let bx = x + ox as i32;
1031 let by = y + oy as i32;
1032 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
1033 continue;
1034 }
1035 let src = overlay.get_pixel(ox, oy);
1036 let dst = base.get_pixel(bx as u32, by as u32);
1037 let src_alpha = (src[3] as f32 / 255.0) * opacity;
1038 if src_alpha < 1e-6 {
1039 continue;
1040 }
1041 let overlay_channel = |s: u8, d: u8| -> u8 {
1043 if d <= 128 {
1044 (2 * s as u16 * d as u16 / 255) as u8
1045 } else {
1046 (255 - (2 * (255 - s) as u16 * (255 - d) as u16 / 255)) as u8
1047 }
1048 };
1049 let ov_r = overlay_channel(src[0], dst[0]);
1050 let ov_g = overlay_channel(src[1], dst[1]);
1051 let ov_b = overlay_channel(src[2], dst[2]);
1052 let dst_alpha = dst[3] as f32 / 255.0;
1053 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1054 if out_alpha < 1e-6 {
1055 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1056 continue;
1057 }
1058 let out_r = ((ov_r as f32 * src_alpha + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1059 / out_alpha) as u8;
1060 let out_g = ((ov_g as f32 * src_alpha + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1061 / out_alpha) as u8;
1062 let out_b = ((ov_b as f32 * src_alpha + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1063 / out_alpha) as u8;
1064 let out_a = (out_alpha * 255.0) as u8;
1065 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1066 }
1067 }
1068}
1069
1070fn blend_screen(base: &mut RgbaImage, overlay: &RgbaImage, x: i32, y: i32, opacity: f32) {
1072 let (w1, h1) = base.dimensions();
1073 let (w2, h2) = overlay.dimensions();
1074 let opacity = opacity.clamp(0.0, 1.0);
1075
1076 for oy in 0..h2 {
1077 for ox in 0..w2 {
1078 let bx = x + ox as i32;
1079 let by = y + oy as i32;
1080 if bx < 0 || by < 0 || bx >= w1 as i32 || by >= h1 as i32 {
1081 continue;
1082 }
1083 let src = overlay.get_pixel(ox, oy);
1084 let dst = base.get_pixel(bx as u32, by as u32);
1085 let src_alpha = (src[3] as f32 / 255.0) * opacity;
1086 if src_alpha < 1e-6 {
1087 continue;
1088 }
1089 let screen_r = (255 - (255 - src[0]) as u16 * (255 - dst[0]) as u16 / 255) as u8;
1091 let screen_g = (255 - (255 - src[1]) as u16 * (255 - dst[1]) as u16 / 255) as u8;
1092 let screen_b = (255 - (255 - src[2]) as u16 * (255 - dst[2]) as u16 / 255) as u8;
1093 let dst_alpha = dst[3] as f32 / 255.0;
1094 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
1095 if out_alpha < 1e-6 {
1096 base.put_pixel(bx as u32, by as u32, Rgba([0, 0, 0, 0]));
1097 continue;
1098 }
1099 let out_r = ((screen_r as f32 * src_alpha
1100 + dst[0] as f32 * dst_alpha * (1.0 - src_alpha))
1101 / out_alpha) as u8;
1102 let out_g = ((screen_g as f32 * src_alpha
1103 + dst[1] as f32 * dst_alpha * (1.0 - src_alpha))
1104 / out_alpha) as u8;
1105 let out_b = ((screen_b as f32 * src_alpha
1106 + dst[2] as f32 * dst_alpha * (1.0 - src_alpha))
1107 / out_alpha) as u8;
1108 let out_a = (out_alpha * 255.0) as u8;
1109 base.put_pixel(bx as u32, by as u32, Rgba([out_r, out_g, out_b, out_a]));
1110 }
1111 }
1112}
1113
1114fn load_font(font_path: Option<&Path>) -> Result<FontVec, ImageError> {
1122 match font_path {
1123 Some(path) => {
1124 let data = std::fs::read(path)
1125 .map_err(|e| ImageError::FontLoadFailed(format!("{path:?}: {e}")))?;
1126 Ok(FontVec::try_from_vec(data)
1127 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font {path:?}: {e}")))?)
1128 }
1129 None => {
1130 Err(ImageError::FontLoadFailed(
1132 "font_path is required (no default font available)".to_string(),
1133 ))
1134 }
1135 }
1136}
1137
1138pub fn measure_text(font_path: &Path, size: u32, text: &str) -> Result<TextMetrics, ImageError> {
1154 let data = std::fs::read(font_path)
1155 .map_err(|e| ImageError::FontLoadFailed(format!("{font_path:?}: {e}")))?;
1156 let font = FontVec::try_from_vec(data)
1157 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font: {e}")))?;
1158 Ok(measure_text_with_font(&font, size, text))
1159}
1160
1161fn measure_text_with_font<F: Font>(font: &F, size: u32, text: &str) -> TextMetrics {
1163 let scale = PxScale::from(size as f32);
1164 let scaled = font.as_scaled(scale);
1165 let ascent = scaled.ascent();
1166 let descent = scaled.descent();
1167 let height = (ascent - descent).ceil();
1168
1169 let mut width: f32 = 0.0;
1170 let mut prev_glyph: Option<Glyph> = None;
1171 for ch in text.chars() {
1172 let glyph = scaled.scaled_glyph(ch);
1173 if let Some(prev) = prev_glyph {
1174 width += scaled.kern(prev.id, glyph.id);
1175 }
1176 width += scaled.h_advance(glyph.id);
1177 prev_glyph = Some(glyph);
1178 }
1179
1180 TextMetrics {
1181 width: width.ceil() as i32,
1182 height: height.ceil() as i32,
1183 ascent: ascent.ceil() as i32,
1184 descent: descent.ceil() as i32,
1185 }
1186}
1187
1188#[derive(Debug, Clone, Copy)]
1190pub struct TextMetrics {
1191 pub width: i32,
1193 pub height: i32,
1195 pub ascent: i32,
1197 pub descent: i32,
1199}
1200
1201pub fn wrap_text(
1245 font_path: &Path,
1246 fontsize: u32,
1247 string: &str,
1248 width: i32,
1249 max_line: Option<usize>,
1250) -> Result<String, ImageError> {
1251 let data = std::fs::read(font_path)
1252 .map_err(|e| ImageError::FontLoadFailed(format!("{font_path:?}: {e}")))?;
1253 let font = FontVec::try_from_vec(data)
1254 .map_err(|e| ImageError::FontLoadFailed(format!("Invalid font: {e}")))?;
1255 Ok(wrap_text_with_font(
1256 &font, fontsize, string, width, max_line,
1257 ))
1258}
1259
1260fn wrap_text_with_font<F: Font>(
1262 font: &F,
1263 fontsize: u32,
1264 string: &str,
1265 width: i32,
1266 max_line: Option<usize>,
1267) -> String {
1268 let mut content = String::new();
1269 let mut line_count: usize = 0;
1270 for l in string.chars() {
1271 let test = format!("{content} {l}");
1273 let metrics = measure_text_with_font(font, fontsize, &test);
1274 if metrics.width > width && !content.is_empty() {
1276 line_count += 1;
1277 if let Some(ml) = max_line {
1278 if line_count >= ml {
1279 let trimmed: String =
1281 content.chars().take(content.chars().count() - 1).collect();
1282 content = format!("{trimmed}...");
1283 break;
1284 }
1285 }
1286 content.push('\n');
1287 }
1288 content.push(l);
1289 }
1290 content
1291}
1292
1293#[cfg(test)]
1298mod tests {
1299 use super::*;
1300
1301 #[test]
1304 fn test_image_type_as_str() {
1305 assert_eq!(ImageType::Unknown.as_str(), "");
1306 assert_eq!(ImageType::Gif.as_str(), "GIF");
1307 assert_eq!(ImageType::Jpeg.as_str(), "JPEG");
1308 assert_eq!(ImageType::Png.as_str(), "PNG");
1309 assert_eq!(ImageType::Wbmp.as_str(), "WBMP");
1310 }
1311
1312 #[test]
1313 fn test_image_type_from_extension() {
1314 assert_eq!(ImageType::from_extension("gif"), ImageType::Gif);
1315 assert_eq!(ImageType::from_extension("jpg"), ImageType::Jpeg);
1316 assert_eq!(ImageType::from_extension("jpeg"), ImageType::Jpeg);
1317 assert_eq!(ImageType::from_extension("png"), ImageType::Png);
1318 assert_eq!(ImageType::from_extension("wbmp"), ImageType::Wbmp);
1319 assert_eq!(ImageType::from_extension("unknown"), ImageType::Unknown);
1320 }
1321
1322 #[test]
1323 fn test_image_type_default() {
1324 assert_eq!(ImageType::default(), ImageType::Unknown);
1325 }
1326
1327 #[test]
1328 fn test_image_type_from_image_format() {
1329 use image::ImageFormat;
1330 assert_eq!(
1331 ImageType::from_image_format(ImageFormat::Gif),
1332 ImageType::Gif
1333 );
1334 assert_eq!(
1335 ImageType::from_image_format(ImageFormat::Jpeg),
1336 ImageType::Jpeg
1337 );
1338 assert_eq!(
1339 ImageType::from_image_format(ImageFormat::Png),
1340 ImageType::Png
1341 );
1342 assert_eq!(
1343 ImageType::from_image_format(ImageFormat::WebP),
1344 ImageType::Unknown
1345 );
1346 }
1347
1348 #[test]
1349 fn test_image_type_to_image_format() {
1350 assert_eq!(
1351 ImageType::Gif.to_image_format(),
1352 Some(image::ImageFormat::Gif)
1353 );
1354 assert_eq!(
1355 ImageType::Jpeg.to_image_format(),
1356 Some(image::ImageFormat::Jpeg)
1357 );
1358 assert_eq!(
1359 ImageType::Png.to_image_format(),
1360 Some(image::ImageFormat::Png)
1361 );
1362 assert_eq!(ImageType::Wbmp.to_image_format(), None);
1363 assert_eq!(ImageType::Unknown.to_image_format(), None);
1364 }
1365
1366 #[test]
1367 fn test_image_type_from_extension_case_insensitive() {
1368 assert_eq!(ImageType::from_extension("GIF"), ImageType::Gif);
1369 assert_eq!(ImageType::from_extension("PNG"), ImageType::Png);
1370 assert_eq!(ImageType::from_extension("JPG"), ImageType::Jpeg);
1371 }
1372
1373 #[test]
1376 fn test_color_rgb() {
1377 let c = Color::rgb(255, 128, 0);
1378 assert_eq!(c.r, 255);
1379 assert_eq!(c.g, 128);
1380 assert_eq!(c.b, 0);
1381 assert_eq!(c.a, 255); }
1383
1384 #[test]
1385 fn test_color_rgba() {
1386 let c = Color::rgba(255, 128, 0, 128);
1387 assert_eq!(c.r, 255);
1388 assert_eq!(c.g, 128);
1389 assert_eq!(c.b, 0);
1390 assert_eq!(c.a, 128);
1391 }
1392
1393 #[test]
1394 fn test_color_from_hex_rrggbb() {
1395 let c = Color::from_hex("#ff8000").unwrap();
1396 assert_eq!(c.r, 255);
1397 assert_eq!(c.g, 128);
1398 assert_eq!(c.b, 0);
1399 assert_eq!(c.a, 255);
1400 }
1401
1402 #[test]
1403 fn test_color_from_hex_rgb() {
1404 let c = Color::from_hex("#f80").unwrap();
1405 assert_eq!(c.r, 255);
1406 assert_eq!(c.g, 136);
1407 assert_eq!(c.b, 0);
1408 assert_eq!(c.a, 255);
1409 }
1410
1411 #[test]
1412 fn test_color_from_hex_rrggbbaa() {
1413 let c = Color::from_hex("#ff800080").unwrap();
1414 assert_eq!(c.r, 255);
1415 assert_eq!(c.g, 128);
1416 assert_eq!(c.b, 0);
1417 assert_eq!(c.a, 128);
1418 }
1419
1420 #[test]
1421 fn test_color_from_hex_no_hash() {
1422 let c = Color::from_hex("ff8000").unwrap();
1423 assert_eq!(c.r, 255);
1424 assert_eq!(c.g, 128);
1425 assert_eq!(c.b, 0);
1426 }
1427
1428 #[test]
1429 fn test_color_from_hex_invalid() {
1430 assert!(Color::from_hex("#xyz").is_err());
1431 assert!(Color::from_hex("#1").is_err());
1432 assert!(Color::from_hex("12345").is_err());
1433 }
1434
1435 #[test]
1436 fn test_color_to_rgba() {
1437 let c = Color::rgb(1, 2, 3);
1438 assert_eq!(c.to_rgba(), Rgba([1, 2, 3, 255]));
1439 }
1440
1441 #[test]
1442 fn test_color_default() {
1443 let c = Color::default();
1444 assert_eq!(c, Color::rgb(0, 0, 0));
1445 }
1446
1447 #[test]
1450 fn test_position_parse() {
1451 assert_eq!(Position::parse("top-left").unwrap(), Position::TopLeft);
1452 assert_eq!(Position::parse("top-center").unwrap(), Position::TopCenter);
1453 assert_eq!(Position::parse("TOP-RIGHT").unwrap(), Position::TopRight);
1454 assert_eq!(Position::parse("center").unwrap(), Position::Center);
1455 assert_eq!(
1456 Position::parse("bottom-right").unwrap(),
1457 Position::BottomRight
1458 );
1459 }
1460
1461 #[test]
1462 fn test_position_parse_invalid() {
1463 assert!(Position::parse("invalid").is_err());
1464 assert!(Position::parse("").is_err());
1465 }
1466
1467 #[test]
1468 fn test_position_as_str() {
1469 assert_eq!(Position::TopLeft.as_str(), "top-left");
1470 assert_eq!(Position::Center.as_str(), "center");
1471 assert_eq!(Position::BottomRight.as_str(), "bottom-right");
1472 }
1473
1474 #[test]
1475 fn test_position_get_xy_top_left() {
1476 let (x, y) = Position::TopLeft.get_xy(100, 100, 20, 20);
1478 assert_eq!(x, 0);
1479 assert_eq!(y, 0);
1480 }
1481
1482 #[test]
1483 fn test_position_get_xy_center() {
1484 let (x, y) = Position::Center.get_xy(100, 100, 20, 20);
1486 assert_eq!(x, 40);
1487 assert_eq!(y, 40);
1488 }
1489
1490 #[test]
1491 fn test_position_get_xy_bottom_right() {
1492 let (x, y) = Position::BottomRight.get_xy(100, 100, 20, 20);
1494 assert_eq!(x, 80);
1495 assert_eq!(y, 80);
1496 }
1497
1498 #[test]
1499 fn test_position_get_xy_top_center() {
1500 let (x, y) = Position::TopCenter.get_xy(100, 100, 20, 20);
1501 assert_eq!(x, 40); assert_eq!(y, 0);
1503 }
1504
1505 fn create_test_png(path: &Path, w: u32, h: u32, color: Rgba<u8>) {
1508 let img: RgbaImage = ImageBuffer::from_pixel(w, h, color);
1509 img.save(path).unwrap();
1510 }
1511
1512 #[test]
1513 fn test_image_create_blank() {
1514 let img = Image::create_blank(100, 50);
1515 assert_eq!(img.width(), 100);
1516 assert_eq!(img.height(), 50);
1517 assert_eq!(img.image_type(), ImageType::Unknown);
1518 assert!(img.file_path().is_none());
1519 }
1520
1521 #[test]
1522 fn test_image_open_png() {
1523 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1524 let path = tmp.path();
1525 create_test_png(path, 80, 60, Rgba([255, 0, 0, 255]));
1526 let img = Image::open(path).unwrap();
1527 assert_eq!(img.width(), 80);
1528 assert_eq!(img.height(), 60);
1529 assert_eq!(img.image_type(), ImageType::Png);
1530 assert!(img.file_path().is_some());
1531 }
1532
1533 #[test]
1534 fn test_image_from_dynamic() {
1535 let buf: RgbaImage = ImageBuffer::from_pixel(50, 50, Rgba([0, 255, 0, 255]));
1536 let dyn_img = DynamicImage::ImageRgba8(buf);
1537 let img = Image::from_dynamic(dyn_img, ImageType::Png);
1538 assert_eq!(img.width(), 50);
1539 assert_eq!(img.height(), 50);
1540 assert_eq!(img.image_type(), ImageType::Png);
1541 }
1542
1543 #[test]
1544 fn test_image_to_rgba8() {
1545 let img = Image::create_blank(30, 30);
1546 let rgba = img.to_rgba8();
1547 assert_eq!(rgba.dimensions(), (30, 30));
1548 }
1549
1550 #[test]
1553 fn test_editor_new() {
1554 let _editor = Editor::new();
1555 let _editor2 = Editor;
1556 }
1557
1558 #[test]
1559 fn test_editor_open() {
1560 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1561 create_test_png(tmp.path(), 100, 100, Rgba([0, 0, 255, 255]));
1562 let editor = Editor::new();
1563 let img = editor.open(tmp.path()).unwrap();
1564 assert_eq!(img.width(), 100);
1565 assert_eq!(img.height(), 100);
1566 }
1567
1568 #[test]
1569 fn test_editor_save_png() {
1570 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1571 create_test_png(tmp_in.path(), 50, 50, Rgba([0, 255, 0, 255]));
1572 let tmp_out = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1573 let editor = Editor::new();
1574 let img = editor.open(tmp_in.path()).unwrap();
1575 editor
1576 .save(&img, tmp_out.path(), None, None, false, 0o755)
1577 .unwrap();
1578 let reopened = image::open(tmp_out.path()).unwrap();
1580 assert_eq!(reopened.width(), 50);
1581 assert_eq!(reopened.height(), 50);
1582 }
1583
1584 #[test]
1585 fn test_editor_save_jpeg_with_quality() {
1586 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1587 create_test_png(tmp_in.path(), 50, 50, Rgba([128, 64, 32, 255]));
1588 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
1589 let editor = Editor::new();
1590 let img = editor.open(tmp_in.path()).unwrap();
1591 editor
1592 .save(&img, tmp_out.path(), None, Some(90), false, 0o755)
1593 .unwrap();
1594 let reopened = image::open(tmp_out.path()).unwrap();
1595 assert_eq!(reopened.width(), 50);
1596 assert_eq!(reopened.height(), 50);
1597 }
1598
1599 #[test]
1602 fn test_editor_resize_exact() {
1603 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1604 create_test_png(tmp.path(), 100, 100, Rgba([255, 0, 0, 255]));
1605 let editor = Editor::new();
1606 let mut img = editor.open(tmp.path()).unwrap();
1607 editor.resize_exact(&mut img, 50, 80);
1608 assert_eq!(img.width(), 50);
1609 assert_eq!(img.height(), 80);
1610 }
1611
1612 #[test]
1613 fn test_editor_resize_fit() {
1614 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1615 create_test_png(tmp.path(), 200, 100, Rgba([0, 255, 0, 255]));
1616 let editor = Editor::new();
1617 let mut img = editor.open(tmp.path()).unwrap();
1618 editor.resize_fit(&mut img, 100, 100);
1620 assert_eq!(img.width(), 100);
1621 assert_eq!(img.height(), 50);
1622 }
1623
1624 #[test]
1625 fn test_editor_resize_fill() {
1626 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1627 create_test_png(tmp.path(), 200, 100, Rgba([0, 0, 255, 255]));
1628 let editor = Editor::new();
1629 let mut img = editor.open(tmp.path()).unwrap();
1630 editor.resize_fill(&mut img, 100, 100);
1632 assert_eq!(img.width(), 100);
1633 assert_eq!(img.height(), 100);
1634 }
1635
1636 #[test]
1637 fn test_editor_resize_exact_width() {
1638 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1639 create_test_png(tmp.path(), 200, 100, Rgba([255, 255, 0, 255]));
1640 let editor = Editor::new();
1641 let mut img = editor.open(tmp.path()).unwrap();
1642 editor.resize_exact_width(&mut img, 50);
1644 assert_eq!(img.width(), 50);
1645 assert_eq!(img.height(), 25);
1646 }
1647
1648 #[test]
1649 fn test_editor_resize_exact_height() {
1650 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1651 create_test_png(tmp.path(), 200, 100, Rgba([255, 0, 255, 255]));
1652 let editor = Editor::new();
1653 let mut img = editor.open(tmp.path()).unwrap();
1654 editor.resize_exact_height(&mut img, 50);
1656 assert_eq!(img.width(), 100);
1657 assert_eq!(img.height(), 50);
1658 }
1659
1660 #[test]
1663 fn test_editor_crop_center() {
1664 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1665 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
1666 let editor = Editor::new();
1667 let mut img = editor.open(tmp.path()).unwrap();
1668 editor
1669 .crop(&mut img, 50, 50, Position::Center, 0, 0)
1670 .unwrap();
1671 assert_eq!(img.width(), 50);
1672 assert_eq!(img.height(), 50);
1673 }
1674
1675 #[test]
1676 fn test_editor_crop_too_large() {
1677 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1678 create_test_png(tmp.path(), 50, 50, Rgba([0, 0, 0, 255]));
1679 let editor = Editor::new();
1680 let mut img = editor.open(tmp.path()).unwrap();
1681 assert!(editor
1682 .crop(&mut img, 100, 100, Position::TopLeft, 0, 0)
1683 .is_err());
1684 }
1685
1686 #[test]
1687 fn test_editor_flip_horizontal() {
1688 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1689 create_test_png(tmp.path(), 80, 60, Rgba([255, 128, 64, 255]));
1690 let editor = Editor::new();
1691 let mut img = editor.open(tmp.path()).unwrap();
1692 editor.flip(&mut img, FlipMode::Horizontal);
1693 assert_eq!(img.width(), 80);
1694 assert_eq!(img.height(), 60);
1695 }
1696
1697 #[test]
1698 fn test_editor_flip_vertical() {
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()).unwrap();
1703 editor.flip(&mut img, FlipMode::Vertical);
1704 assert_eq!(img.width(), 80);
1705 assert_eq!(img.height(), 60);
1706 }
1707
1708 #[test]
1709 fn test_editor_rotate_90() {
1710 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1711 create_test_png(tmp.path(), 80, 60, Rgba([64, 255, 128, 255]));
1712 let editor = Editor::new();
1713 let mut img = editor.open(tmp.path()).unwrap();
1714 editor.rotate(&mut img, 90.0).unwrap();
1715 assert_eq!(img.width(), 60); assert_eq!(img.height(), 80);
1717 }
1718
1719 #[test]
1720 fn test_editor_rotate_180() {
1721 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1722 create_test_png(tmp.path(), 80, 60, Rgba([64, 128, 255, 255]));
1723 let editor = Editor::new();
1724 let mut img = editor.open(tmp.path()).unwrap();
1725 editor.rotate(&mut img, 180.0).unwrap();
1726 assert_eq!(img.width(), 80);
1727 assert_eq!(img.height(), 60);
1728 }
1729
1730 #[test]
1731 fn test_editor_rotate_invalid_angle() {
1732 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1733 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
1734 let editor = Editor::new();
1735 let mut img = editor.open(tmp.path()).unwrap();
1736 assert!(editor.rotate(&mut img, 45.0).is_err());
1737 }
1738
1739 #[test]
1742 fn test_editor_blend_normal() {
1743 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1744 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1745 create_test_png(tmp1.path(), 100, 100, Rgba([0, 0, 0, 255]));
1746 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1747 let editor = Editor::new();
1748 let mut img1 = editor.open(tmp1.path()).unwrap();
1749 let img2 = editor.open(tmp2.path()).unwrap();
1750 editor
1751 .blend(
1752 &mut img1,
1753 &img2,
1754 BlendType::Normal,
1755 1.0,
1756 Position::TopLeft,
1757 0,
1758 0,
1759 )
1760 .unwrap();
1761 assert_eq!(img1.width(), 100);
1762 assert_eq!(img1.height(), 100);
1763 let rgba = img1.to_rgba8();
1765 let pixel = rgba.get_pixel(0, 0);
1766 assert_eq!(pixel[0], 255);
1767 assert_eq!(pixel[1], 255);
1768 assert_eq!(pixel[2], 255);
1769 }
1770
1771 #[test]
1772 fn test_editor_blend_with_offset() {
1773 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1774 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1775 create_test_png(tmp1.path(), 100, 100, Rgba([0, 0, 0, 255]));
1776 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1777 let editor = Editor::new();
1778 let mut img1 = editor.open(tmp1.path()).unwrap();
1779 let img2 = editor.open(tmp2.path()).unwrap();
1780 editor
1782 .blend(
1783 &mut img1,
1784 &img2,
1785 BlendType::Normal,
1786 1.0,
1787 Position::TopLeft,
1788 30,
1789 30,
1790 )
1791 .unwrap();
1792 let rgba = img1.to_rgba8();
1793 let p1 = rgba.get_pixel(0, 0);
1795 assert_eq!(p1[0], 0);
1796 let p2 = rgba.get_pixel(50, 50);
1798 assert_eq!(p2[0], 255);
1799 let p3 = rgba.get_pixel(90, 90);
1801 assert_eq!(p3[0], 0);
1802 }
1803
1804 #[test]
1805 fn test_editor_blend_opacity_half() {
1806 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1807 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1808 create_test_png(tmp1.path(), 50, 50, Rgba([0, 0, 0, 255]));
1809 create_test_png(tmp2.path(), 50, 50, Rgba([255, 255, 255, 255]));
1810 let editor = Editor::new();
1811 let mut img1 = editor.open(tmp1.path()).unwrap();
1812 let img2 = editor.open(tmp2.path()).unwrap();
1813 editor
1815 .blend(
1816 &mut img1,
1817 &img2,
1818 BlendType::Normal,
1819 0.5,
1820 Position::TopLeft,
1821 0,
1822 0,
1823 )
1824 .unwrap();
1825 let rgba = img1.to_rgba8();
1826 let pixel = rgba.get_pixel(0, 0);
1827 assert!(
1829 (120..=136).contains(&pixel[0]),
1830 "expected ~128, got {}",
1831 pixel[0]
1832 );
1833 }
1834
1835 #[test]
1836 fn test_blend_type_parse() {
1837 assert_eq!(BlendType::parse("normal").unwrap(), BlendType::Normal);
1838 assert_eq!(BlendType::parse("MULTIPLY").unwrap(), BlendType::Multiply);
1839 assert_eq!(BlendType::parse("overlay").unwrap(), BlendType::Overlay);
1840 assert_eq!(BlendType::parse("screen").unwrap(), BlendType::Screen);
1841 assert!(BlendType::parse("invalid").is_err());
1842 }
1843
1844 #[test]
1845 fn test_flip_mode_parse() {
1846 assert_eq!(FlipMode::parse("h").unwrap(), FlipMode::Horizontal);
1847 assert_eq!(FlipMode::parse("V").unwrap(), FlipMode::Vertical);
1848 assert!(FlipMode::parse("x").is_err());
1849 }
1850
1851 #[test]
1854 fn test_editor_fill() {
1855 let img = Image::create_blank(50, 50);
1856 let editor = Editor::new();
1857 let mut img = img;
1858 editor.fill(&mut img, Color::rgb(255, 0, 0));
1859 let rgba = img.to_rgba8();
1860 let pixel = rgba.get_pixel(0, 0);
1861 assert_eq!(pixel[0], 255);
1862 assert_eq!(pixel[1], 0);
1863 assert_eq!(pixel[2], 0);
1864 }
1865
1866 #[test]
1870 fn test_r5_24_image_type_constants() {
1871 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"); }
1878
1879 #[test]
1881 fn test_r5_25_color_hex_parsing() {
1882 let c1 = Color::from_hex("#333333").unwrap();
1884 assert_eq!((c1.r, c1.g, c1.b), (0x33, 0x33, 0x33));
1885 let c2 = Color::from_hex("#ff4444").unwrap();
1887 assert_eq!((c2.r, c2.g, c2.b), (0xff, 0x44, 0x44));
1888 let c3 = Color::from_hex("#f00").unwrap();
1890 assert_eq!((c3.r, c3.g, c3.b), (0xff, 0x00, 0x00));
1891 }
1892
1893 #[test]
1895 fn test_r5_26_position_get_xy_all_nine() {
1896 let w1 = 100u32;
1897 let h1 = 100u32;
1898 let w2 = 20u32;
1899 let h2 = 20u32;
1900 let cases = [
1902 (Position::TopLeft, 0, 0),
1903 (Position::TopCenter, 40, 0),
1904 (Position::TopRight, 80, 0),
1905 (Position::CenterLeft, 0, 40),
1906 (Position::Center, 40, 40),
1907 (Position::CenterRight, 80, 40),
1908 (Position::BottomLeft, 0, 80),
1909 (Position::BottomCenter, 40, 80),
1910 (Position::BottomRight, 80, 80),
1911 ];
1912 for (pos, ex, ey) in cases {
1913 let (x, y) = pos.get_xy(w1, h1, w2, h2);
1914 assert_eq!(x, ex, "Position {:?} x mismatch", pos);
1915 assert_eq!(y, ey, "Position {:?} y mismatch", pos);
1916 }
1917 }
1918
1919 #[test]
1921 fn test_r5_27_image_open_detects_type() {
1922 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1923 create_test_png(tmp.path(), 80, 60, Rgba([255, 255, 255, 255]));
1924 let img = Image::open(tmp.path()).unwrap();
1925 assert_eq!(img.image_type(), ImageType::Png);
1926 assert_eq!(img.width(), 80);
1927 assert_eq!(img.height(), 60);
1928 }
1929
1930 #[test]
1932 fn test_r5_28_resize_exact_forces_dimensions() {
1933 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1934 create_test_png(tmp.path(), 200, 100, Rgba([255, 0, 0, 255]));
1935 let editor = Editor::new();
1936 let mut img = editor.open(tmp.path()).unwrap();
1937 editor.resize_exact(&mut img, 50, 50);
1939 assert_eq!(img.width(), 50);
1940 assert_eq!(img.height(), 50);
1941 }
1942
1943 #[test]
1945 fn test_r5_29_blend_normal_with_offset_and_opacity() {
1946 let tmp1 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1947 let tmp2 = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1948 create_test_png(tmp1.path(), 200, 200, Rgba([0, 0, 0, 255]));
1949 create_test_png(tmp2.path(), 100, 100, Rgba([255, 255, 255, 255]));
1950 let editor = Editor::new();
1951 let mut img1 = editor.open(tmp1.path()).unwrap();
1952 let img2 = editor.open(tmp2.path()).unwrap();
1953 editor
1955 .blend(
1956 &mut img1,
1957 &img2,
1958 BlendType::Normal,
1959 1.0,
1960 Position::TopLeft,
1961 30,
1962 30,
1963 )
1964 .unwrap();
1965 let rgba = img1.to_rgba8();
1967 assert_eq!(rgba.get_pixel(0, 0)[0], 0);
1969 assert_eq!(rgba.get_pixel(50, 50)[0], 255);
1971 assert_eq!(rgba.get_pixel(129, 129)[0], 255);
1973 assert_eq!(rgba.get_pixel(130, 130)[0], 0);
1975 assert_eq!(rgba.get_pixel(150, 150)[0], 0);
1977 }
1978
1979 #[test]
1981 fn test_r5_30_text_y_baseline_offset() {
1982 let mut img = Image::create_blank(200, 100);
1987 let editor = Editor::new();
1988 let result = editor.text(&mut img, "test", 30, 10, 50, Color::rgb(0, 0, 0), None);
1990 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
1991 }
1992
1993 #[test]
1995 fn test_r5_31_save_infers_type_from_extension() {
1996 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
1997 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 128, 64, 255]));
1998
1999 let tmp_png = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2001 let editor = Editor::new();
2002 let img = editor.open(tmp_in.path()).unwrap();
2003 editor
2004 .save(&img, tmp_png.path(), None, None, false, 0o755)
2005 .unwrap();
2006 assert!(tmp_png.path().exists());
2007
2008 let tmp_jpg = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2010 editor
2011 .save(&img, tmp_jpg.path(), None, None, false, 0o755)
2012 .unwrap();
2013 assert!(tmp_jpg.path().exists());
2014 }
2015
2016 #[test]
2018 fn test_r5_32_wrap_text_signature_alignment() {
2019 let result = wrap_text(Path::new("/nonexistent.ttf"), 30, "hello", 680, Some(2));
2023 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2024 }
2025
2026 #[test]
2028 fn test_r5_32_wrap_text_with_font_logic() {
2029 let font_path = Path::new(
2032 "e:/vue/test/鲜视达/server/vendor/kosinix/grafika/src/Grafika/fonts/st-heiti-light.ttc",
2033 );
2034 if !font_path.exists() {
2035 eprintln!(
2037 "Skipping test_r5_32_wrap_text_with_font_logic: font not found at {font_path:?}"
2038 );
2039 return;
2040 }
2041 let data = std::fs::read(font_path).unwrap();
2042 let font = FontVec::try_from_vec(data).unwrap();
2043 let result = wrap_text_with_font(&font, 30, "hello", 680, Some(2));
2045 assert_eq!(result, "hello");
2046 let long_text = "这是一个非常长的商品名称用于测试自动换行功能应该被截断并添加省略号";
2048 let result = wrap_text_with_font(&font, 30, long_text, 100, Some(2));
2049 assert!(
2050 result.ends_with("..."),
2051 "result should end with ..., got: {result}"
2052 );
2053 assert!(
2054 result.contains('\n'),
2055 "result should contain newline, got: {result}"
2056 );
2057 }
2058
2059 #[test]
2062 fn test_measure_text_nonexistent_font() {
2063 let result = measure_text(Path::new("/nonexistent.ttf"), 30, "hello");
2064 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2065 }
2066
2067 #[test]
2068 fn test_text_metrics_debug() {
2069 let m = TextMetrics {
2070 width: 100,
2071 height: 30,
2072 ascent: 25,
2073 descent: -5,
2074 };
2075 assert_eq!(m.width, 100);
2076 assert_eq!(m.height, 30);
2077 }
2078
2079 #[test]
2082 fn test_color_from_hex_rgba_short() {
2083 let c = Color::from_hex("#f80f").unwrap();
2084 assert_eq!(c.r, 255);
2085 assert_eq!(c.g, 136);
2086 assert_eq!(c.b, 0);
2087 assert_eq!(c.a, 255);
2088 }
2089
2090 #[test]
2091 fn test_color_from_hex_with_whitespace() {
2092 let c = Color::from_hex(" #ff8000 ").unwrap();
2093 assert_eq!(c.r, 255);
2094 assert_eq!(c.g, 128);
2095 assert_eq!(c.b, 0);
2096 }
2097
2098 #[test]
2099 fn test_color_from_hex_empty() {
2100 assert!(Color::from_hex("#").is_err());
2101 assert!(Color::from_hex("").is_err());
2102 }
2103
2104 #[test]
2105 fn test_color_from_hex_invalid_chars() {
2106 assert!(Color::from_hex("#gggggg").is_err());
2107 assert!(Color::from_hex("#zz").is_err());
2108 }
2109
2110 #[test]
2111 fn test_color_copy_and_eq() {
2112 let c1 = Color::rgb(1, 2, 3);
2113 let c2 = c1;
2114 assert_eq!(c1, c2);
2115 }
2116
2117 #[test]
2120 fn test_image_type_from_image_format_other() {
2121 use image::ImageFormat;
2122 assert_eq!(
2123 ImageType::from_image_format(ImageFormat::Bmp),
2124 ImageType::Unknown
2125 );
2126 assert_eq!(
2127 ImageType::from_image_format(ImageFormat::Tiff),
2128 ImageType::Unknown
2129 );
2130 }
2131
2132 #[test]
2133 fn test_image_type_from_extension_empty() {
2134 assert_eq!(ImageType::from_extension(""), ImageType::Unknown);
2135 }
2136
2137 #[test]
2140 fn test_position_parse_all_nine() {
2141 assert_eq!(Position::parse("top-left").unwrap(), Position::TopLeft);
2142 assert_eq!(Position::parse("top-center").unwrap(), Position::TopCenter);
2143 assert_eq!(Position::parse("top-right").unwrap(), Position::TopRight);
2144 assert_eq!(
2145 Position::parse("center-left").unwrap(),
2146 Position::CenterLeft
2147 );
2148 assert_eq!(Position::parse("center").unwrap(), Position::Center);
2149 assert_eq!(
2150 Position::parse("center-right").unwrap(),
2151 Position::CenterRight
2152 );
2153 assert_eq!(
2154 Position::parse("bottom-left").unwrap(),
2155 Position::BottomLeft
2156 );
2157 assert_eq!(
2158 Position::parse("bottom-center").unwrap(),
2159 Position::BottomCenter
2160 );
2161 assert_eq!(
2162 Position::parse("bottom-right").unwrap(),
2163 Position::BottomRight
2164 );
2165 }
2166
2167 #[test]
2168 fn test_position_as_str_all_nine() {
2169 assert_eq!(Position::TopLeft.as_str(), "top-left");
2170 assert_eq!(Position::TopCenter.as_str(), "top-center");
2171 assert_eq!(Position::TopRight.as_str(), "top-right");
2172 assert_eq!(Position::CenterLeft.as_str(), "center-left");
2173 assert_eq!(Position::Center.as_str(), "center");
2174 assert_eq!(Position::CenterRight.as_str(), "center-right");
2175 assert_eq!(Position::BottomLeft.as_str(), "bottom-left");
2176 assert_eq!(Position::BottomCenter.as_str(), "bottom-center");
2177 assert_eq!(Position::BottomRight.as_str(), "bottom-right");
2178 }
2179
2180 #[test]
2181 fn test_position_get_xy_unequal_dimensions() {
2182 let (x, y) = Position::Center.get_xy(200, 100, 40, 30);
2183 assert_eq!(x, 80);
2184 assert_eq!(y, 35);
2185 }
2186
2187 #[test]
2190 fn test_image_from_rgba8() {
2191 let buf: RgbaImage = ImageBuffer::from_pixel(40, 30, Rgba([10, 20, 30, 255]));
2192 let img = Image::from_rgba8(buf, ImageType::Png);
2193 assert_eq!(img.width(), 40);
2194 assert_eq!(img.height(), 30);
2195 assert_eq!(img.image_type(), ImageType::Png);
2196 assert!(img.file_path().is_none());
2197 }
2198
2199 #[test]
2200 fn test_image_as_dynamic() {
2201 let img = Image::create_blank(50, 50);
2202 let dyn_ref = img.as_dynamic();
2203 assert_eq!(dyn_ref.width(), 50);
2204 assert_eq!(dyn_ref.height(), 50);
2205 }
2206
2207 #[test]
2208 fn test_image_as_dynamic_mut() {
2209 let mut img = Image::create_blank(50, 50);
2210 let dyn_mut = img.as_dynamic_mut();
2211 assert_eq!(dyn_mut.width(), 50);
2212 assert_eq!(dyn_mut.height(), 50);
2213 }
2214
2215 #[test]
2216 fn test_image_open_nonexistent() {
2217 let result = Image::open(Path::new("/nonexistent/file.png"));
2218 assert!(result.is_err());
2219 }
2220
2221 #[test]
2222 fn test_image_open_unknown_extension_fails() {
2223 let tmp_png = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2226 create_test_png(tmp_png.path(), 60, 40, Rgba([255, 0, 0, 255]));
2227 let bin_path = tmp_png.path().with_extension("bin");
2228 std::fs::rename(tmp_png.path(), &bin_path).unwrap();
2229 let result = Image::open(&bin_path);
2230 assert!(result.is_err());
2231 }
2232
2233 #[test]
2236 fn test_editor_default() {
2237 let _editor = Editor;
2238 }
2239
2240 #[test]
2241 fn test_editor_rotate_0() {
2242 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2243 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2244 let editor = Editor::new();
2245 let mut img = editor.open(tmp.path()).unwrap();
2246 editor.rotate(&mut img, 0.0).unwrap();
2247 assert_eq!(img.width(), 80);
2248 assert_eq!(img.height(), 60);
2249 }
2250
2251 #[test]
2252 fn test_editor_rotate_270() {
2253 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2254 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2255 let editor = Editor::new();
2256 let mut img = editor.open(tmp.path()).unwrap();
2257 editor.rotate(&mut img, 270.0).unwrap();
2258 assert_eq!(img.width(), 60);
2259 assert_eq!(img.height(), 80);
2260 }
2261
2262 #[test]
2263 fn test_editor_rotate_negative_90() {
2264 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2265 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2266 let editor = Editor::new();
2267 let mut img = editor.open(tmp.path()).unwrap();
2268 editor.rotate(&mut img, -90.0).unwrap();
2269 assert_eq!(img.width(), 60);
2270 assert_eq!(img.height(), 80);
2271 }
2272
2273 #[test]
2274 fn test_editor_rotate_negative_180() {
2275 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2276 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2277 let editor = Editor::new();
2278 let mut img = editor.open(tmp.path()).unwrap();
2279 editor.rotate(&mut img, -180.0).unwrap();
2280 assert_eq!(img.width(), 80);
2281 assert_eq!(img.height(), 60);
2282 }
2283
2284 #[test]
2285 fn test_editor_rotate_360_normalized_to_0() {
2286 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2287 create_test_png(tmp.path(), 80, 60, Rgba([0, 0, 0, 255]));
2288 let editor = Editor::new();
2289 let mut img = editor.open(tmp.path()).unwrap();
2290 editor.rotate(&mut img, 360.0).unwrap();
2291 assert_eq!(img.width(), 80);
2292 assert_eq!(img.height(), 60);
2293 }
2294
2295 #[test]
2296 fn test_editor_crop_with_positive_offset() {
2297 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2298 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2299 let editor = Editor::new();
2300 let mut img = editor.open(tmp.path()).unwrap();
2301 editor
2302 .crop(&mut img, 50, 50, Position::Center, 10, 10)
2303 .unwrap();
2304 assert_eq!(img.width(), 50);
2305 assert_eq!(img.height(), 50);
2306 }
2307
2308 #[test]
2309 fn test_editor_crop_with_negative_offset_clamped() {
2310 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2311 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2312 let editor = Editor::new();
2313 let mut img = editor.open(tmp.path()).unwrap();
2314 editor
2315 .crop(&mut img, 50, 50, Position::TopLeft, -100, -100)
2316 .unwrap();
2317 assert_eq!(img.width(), 50);
2318 assert_eq!(img.height(), 50);
2319 }
2320
2321 #[test]
2322 fn test_editor_crop_top_left() {
2323 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2324 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2325 let editor = Editor::new();
2326 let mut img = editor.open(tmp.path()).unwrap();
2327 editor
2328 .crop(&mut img, 30, 30, Position::TopLeft, 0, 0)
2329 .unwrap();
2330 assert_eq!(img.width(), 30);
2331 assert_eq!(img.height(), 30);
2332 }
2333
2334 #[test]
2335 fn test_editor_crop_bottom_right() {
2336 let tmp = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2337 create_test_png(tmp.path(), 100, 100, Rgba([0, 128, 255, 255]));
2338 let editor = Editor::new();
2339 let mut img = editor.open(tmp.path()).unwrap();
2340 editor
2341 .crop(&mut img, 30, 30, Position::BottomRight, 0, 0)
2342 .unwrap();
2343 assert_eq!(img.width(), 30);
2344 assert_eq!(img.height(), 30);
2345 }
2346
2347 #[test]
2350 fn test_editor_blend_multiply() {
2351 let base = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2352 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2353 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2354 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2355 let editor = Editor::new();
2356 editor
2357 .blend(
2358 &mut img1,
2359 &img2,
2360 BlendType::Multiply,
2361 1.0,
2362 Position::TopLeft,
2363 0,
2364 0,
2365 )
2366 .unwrap();
2367 let rgba = img1.to_rgba8();
2368 let pixel = rgba.get_pixel(0, 0);
2369 assert!(
2370 (60..=68).contains(&pixel[0]),
2371 "expected ~64, got {}",
2372 pixel[0]
2373 );
2374 }
2375
2376 #[test]
2377 fn test_editor_blend_overlay_dark() {
2378 let base = ImageBuffer::from_pixel(50, 50, Rgba([64, 64, 64, 255]));
2379 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2380 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([128, 128, 128, 255]));
2381 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2382 let editor = Editor::new();
2383 editor
2384 .blend(
2385 &mut img1,
2386 &img2,
2387 BlendType::Overlay,
2388 1.0,
2389 Position::TopLeft,
2390 0,
2391 0,
2392 )
2393 .unwrap();
2394 let rgba = img1.to_rgba8();
2395 let pixel = rgba.get_pixel(0, 0);
2396 assert!(
2397 (60..=68).contains(&pixel[0]),
2398 "expected ~64, got {}",
2399 pixel[0]
2400 );
2401 }
2402
2403 #[test]
2404 fn test_editor_blend_overlay_light() {
2405 let base = ImageBuffer::from_pixel(50, 50, Rgba([200, 200, 200, 255]));
2406 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2407 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([200, 200, 200, 255]));
2408 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2409 let editor = Editor::new();
2410 editor
2411 .blend(
2412 &mut img1,
2413 &img2,
2414 BlendType::Overlay,
2415 1.0,
2416 Position::TopLeft,
2417 0,
2418 0,
2419 )
2420 .unwrap();
2421 let rgba = img1.to_rgba8();
2422 let pixel = rgba.get_pixel(0, 0);
2423 assert!(
2424 (225..=235).contains(&pixel[0]),
2425 "expected ~231, got {}",
2426 pixel[0]
2427 );
2428 }
2429
2430 #[test]
2431 fn test_editor_blend_screen() {
2432 let base = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2433 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2434 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2435 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2436 let editor = Editor::new();
2437 editor
2438 .blend(
2439 &mut img1,
2440 &img2,
2441 BlendType::Screen,
2442 1.0,
2443 Position::TopLeft,
2444 0,
2445 0,
2446 )
2447 .unwrap();
2448 let rgba = img1.to_rgba8();
2449 let pixel = rgba.get_pixel(0, 0);
2450 assert_eq!(pixel[0], 0);
2451 }
2452
2453 #[test]
2454 fn test_editor_blend_with_negative_offset() {
2455 let base = ImageBuffer::from_pixel(50, 50, Rgba([0, 0, 0, 255]));
2456 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2457 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([255, 255, 255, 255]));
2458 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2459 let editor = Editor::new();
2460 editor
2461 .blend(
2462 &mut img1,
2463 &img2,
2464 BlendType::Normal,
2465 1.0,
2466 Position::TopLeft,
2467 -25,
2468 -25,
2469 )
2470 .unwrap();
2471 let rgba = img1.to_rgba8();
2472 assert_eq!(rgba.get_pixel(0, 0)[0], 255);
2473 assert_eq!(rgba.get_pixel(24, 24)[0], 255);
2474 assert_eq!(rgba.get_pixel(25, 25)[0], 0);
2475 }
2476
2477 #[test]
2478 fn test_editor_blend_transparent_overlay() {
2479 let base = ImageBuffer::from_pixel(50, 50, Rgba([100, 100, 100, 255]));
2480 let mut img1 = Image::from_rgba8(base, ImageType::Png);
2481 let overlay = ImageBuffer::from_pixel(50, 50, Rgba([255, 0, 0, 0]));
2482 let img2 = Image::from_rgba8(overlay, ImageType::Png);
2483 let editor = Editor::new();
2484 editor
2485 .blend(
2486 &mut img1,
2487 &img2,
2488 BlendType::Normal,
2489 1.0,
2490 Position::TopLeft,
2491 0,
2492 0,
2493 )
2494 .unwrap();
2495 let rgba = img1.to_rgba8();
2496 let pixel = rgba.get_pixel(0, 0);
2497 assert_eq!(pixel[0], 100);
2498 }
2499
2500 #[test]
2503 fn test_editor_save_gif() {
2504 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2505 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2506 let tmp_out = tempfile::Builder::new().suffix(".gif").tempfile().unwrap();
2507 let editor = Editor::new();
2508 let img = editor.open(tmp_in.path()).unwrap();
2509 editor
2510 .save(&img, tmp_out.path(), None, None, false, 0o755)
2511 .unwrap();
2512 assert!(tmp_out.path().exists());
2513 let reopened = image::open(tmp_out.path()).unwrap();
2514 assert_eq!(reopened.width(), 30);
2515 }
2516
2517 #[test]
2518 fn test_editor_save_wbmp_error() {
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(".wbmp").tempfile().unwrap();
2522 let editor = Editor::new();
2523 let img = editor.open(tmp_in.path()).unwrap();
2524 let result = editor.save(&img, tmp_out.path(), None, None, false, 0o755);
2525 assert!(result.is_err());
2526 }
2527
2528 #[test]
2529 fn test_editor_save_unknown_type_error() {
2530 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2531 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2532 let tmp_out = tempfile::Builder::new().suffix(".bin").tempfile().unwrap();
2533 let editor = Editor::new();
2534 let img = editor.open(tmp_in.path()).unwrap();
2535 let result = editor.save(&img, tmp_out.path(), None, None, false, 0o755);
2536 assert!(result.is_err());
2537 }
2538
2539 #[test]
2540 fn test_editor_save_with_explicit_png_type() {
2541 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2542 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2543 let tmp_out = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2544 let editor = Editor::new();
2545 let img = editor.open(tmp_in.path()).unwrap();
2546 editor
2547 .save(
2548 &img,
2549 tmp_out.path(),
2550 Some(ImageType::Png),
2551 None,
2552 false,
2553 0o755,
2554 )
2555 .unwrap();
2556 assert!(tmp_out.path().exists());
2557 }
2558
2559 #[test]
2560 fn test_editor_save_jpeg_explicit_type() {
2561 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2562 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 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()).unwrap();
2566 editor
2567 .save(
2568 &img,
2569 tmp_out.path(),
2570 Some(ImageType::Jpeg),
2571 Some(80),
2572 false,
2573 0o755,
2574 )
2575 .unwrap();
2576 assert!(tmp_out.path().exists());
2577 }
2578
2579 #[test]
2580 fn test_editor_save_quality_clamping_high() {
2581 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2582 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 255]));
2583 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2584 let editor = Editor::new();
2585 let img = editor.open(tmp_in.path()).unwrap();
2586 editor
2587 .save(&img, tmp_out.path(), None, Some(200), false, 0o755)
2588 .unwrap();
2589 assert!(tmp_out.path().exists());
2590 }
2591
2592 #[test]
2593 fn test_editor_save_quality_clamping_zero() {
2594 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2595 create_test_png(tmp_in.path(), 30, 30, Rgba([128, 64, 32, 255]));
2596 let tmp_out = tempfile::Builder::new().suffix(".jpg").tempfile().unwrap();
2597 let editor = Editor::new();
2598 let img = editor.open(tmp_in.path()).unwrap();
2599 editor
2600 .save(&img, tmp_out.path(), None, Some(0), false, 0o755)
2601 .unwrap();
2602 assert!(tmp_out.path().exists());
2603 }
2604
2605 #[test]
2606 fn test_editor_save_creates_parent_dir() {
2607 let tmp_in = tempfile::Builder::new().suffix(".png").tempfile().unwrap();
2608 create_test_png(tmp_in.path(), 30, 30, Rgba([255, 0, 0, 255]));
2609 let tmp_dir = tempfile::tempdir().unwrap();
2610 let output_path = tmp_dir.path().join("subdir").join("output.png");
2611 assert!(!output_path.parent().unwrap().exists());
2612 let editor = Editor::new();
2613 let img = editor.open(tmp_in.path()).unwrap();
2614 editor
2615 .save(&img, &output_path, None, None, false, 0o755)
2616 .unwrap();
2617 assert!(output_path.exists());
2618 }
2619
2620 #[test]
2623 fn test_load_font_invalid_data() {
2624 let tmp = tempfile::Builder::new().suffix(".ttf").tempfile().unwrap();
2625 std::fs::write(tmp.path(), b"this is not a font").unwrap();
2626 let mut img = Image::create_blank(100, 50);
2627 let editor = Editor::new();
2628 let result = editor.text(
2629 &mut img,
2630 "test",
2631 20,
2632 10,
2633 30,
2634 Color::rgb(0, 0, 0),
2635 Some(tmp.path()),
2636 );
2637 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2638 }
2639
2640 #[test]
2641 fn test_measure_text_invalid_font() {
2642 let tmp = tempfile::Builder::new().suffix(".ttf").tempfile().unwrap();
2643 std::fs::write(tmp.path(), b"invalid font data").unwrap();
2644 let result = measure_text(tmp.path(), 30, "hello");
2645 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2646 }
2647
2648 #[test]
2649 fn test_wrap_text_nonexistent_font() {
2650 let result = wrap_text(Path::new("/nonexistent.ttf"), 30, "hello", 680, None);
2651 assert!(matches!(result, Err(ImageError::FontLoadFailed(_))));
2652 }
2653
2654 #[test]
2655 fn test_editor_text_with_font() {
2656 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2657 if !font_path.exists() {
2658 eprintln!("Skipping test_editor_text_with_font: font not found");
2659 return;
2660 }
2661 let mut img = Image::create_blank(200, 100);
2662 let editor = Editor::new();
2663 let result = editor.text(
2664 &mut img,
2665 "hello",
2666 30,
2667 10,
2668 50,
2669 Color::rgb(255, 0, 0),
2670 Some(font_path),
2671 );
2672 assert!(result.is_ok());
2673 assert_eq!(img.width(), 200);
2674 assert_eq!(img.height(), 100);
2675 }
2676
2677 #[test]
2678 fn test_measure_text_with_font() {
2679 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2680 if !font_path.exists() {
2681 eprintln!("Skipping test_measure_text_with_font: font not found");
2682 return;
2683 }
2684 let result = measure_text(font_path, 30, "hello").unwrap();
2685 assert!(result.width > 0);
2686 assert!(result.height > 0);
2687 }
2688
2689 #[test]
2690 fn test_wrap_text_with_font_no_max_line() {
2691 let font_path = Path::new("C:/Windows/Fonts/arial.ttf");
2692 if !font_path.exists() {
2693 eprintln!("Skipping test_wrap_text_with_font_no_max_line: font not found");
2694 return;
2695 }
2696 let data = std::fs::read(font_path).unwrap();
2697 let font = FontVec::try_from_vec(data).unwrap();
2698 let long_text = "this is a very long text that should wrap";
2699 let result = wrap_text_with_font(&font, 30, long_text, 100, None);
2700 assert!(result.contains('\n'), "should contain newline: {result}");
2701 assert!(
2702 !result.ends_with("..."),
2703 "should not end with ... when no max_line"
2704 );
2705 }
2706}