diff --git a/Cargo.toml b/Cargo.toml index 7a09d36..61353eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,4 @@ members = [ "crate" ] +resolver = "2" diff --git a/crate/Cargo.toml b/crate/Cargo.toml index c4bc83f..30fa387 100644 --- a/crate/Cargo.toml +++ b/crate/Cargo.toml @@ -15,25 +15,11 @@ homepage = "https://silvia-odwyer.github.io/photon/" crate-type = ["cdylib", "rlib"] [dependencies] -image = { version = "0.24.8", default-features = false, features = [ - "gif", - "jpeg", - "ico", - "png", - "pnm", - "tga", - "tiff", - "webp", - "bmp", - "hdr", - "dxt", - "dds", - "farbfeld", -] } +image = "0.25.8" palette = "0.6.1" rand = "0.7.2" -imageproc = { version = "0.23.0", default-features = false } -rusttype = "0.9.2" +imageproc = { version = "0.25.0", default-features = false } +ab_glyph = "0.2" base64 = "0.13.0" time = "0.3.21" wasm-bindgen = { version = "0.2.92", optional = true } diff --git a/crate/src/lib.rs b/crate/src/lib.rs index b0ae6fa..2a92b4f 100644 --- a/crate/src/lib.rs +++ b/crate/src/lib.rs @@ -42,6 +42,9 @@ use base64::{decode, encode}; use image::DynamicImage::ImageRgba8; use image::GenericImage; +use image::codecs::jpeg::JpegEncoder; +use image::codecs::avif::AvifEncoder; +use image::{ExtendedColorType, ImageEncoder}; use serde::{Deserialize, Serialize}; use std::io::Cursor; @@ -187,7 +190,7 @@ impl PhotonImage { img = ImageRgba8(img.to_rgba8()); let mut buffer = vec![]; - img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) + img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png) .unwrap(); let base64 = encode(&buffer); @@ -201,19 +204,21 @@ impl PhotonImage { let mut img = helpers::dyn_image_from_raw(self); img = ImageRgba8(img.to_rgba8()); let mut buffer = vec![]; - img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png) - .unwrap(); + img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png).unwrap(); buffer } /// Convert the PhotonImage to raw bytes. Returns a JPEG. pub fn get_bytes_jpeg(&self, quality: u8) -> Vec { - let mut img = helpers::dyn_image_from_raw(self); - img = ImageRgba8(img.to_rgba8()); - let mut buffer = vec![]; - let out_format = image::ImageOutputFormat::Jpeg(quality); - img.write_to(&mut Cursor::new(&mut buffer), out_format) - .unwrap(); + let img = helpers::dyn_image_from_raw(self); + + // JPEG has no alpha; convert to RGB8. + let rgb = img.to_rgb8(); + let (w, h) = rgb.dimensions(); + + let mut buffer = Vec::new(); + let encoder = JpegEncoder::new_with_quality(&mut buffer, quality); + encoder.write_image(rgb.as_raw(), w, h, ExtendedColorType::Rgb8).unwrap(); buffer } @@ -222,9 +227,22 @@ impl PhotonImage { let mut img = helpers::dyn_image_from_raw(self); img = ImageRgba8(img.to_rgba8()); let mut buffer = vec![]; - let out_format = image::ImageOutputFormat::WebP; - img.write_to(&mut Cursor::new(&mut buffer), out_format) - .unwrap(); + let out_format = image::ImageFormat::WebP; + img.write_to(&mut Cursor::new(&mut buffer), out_format).unwrap(); + buffer + } + + /// Convert the PhotonImage to raw bytes. Returns an AVIF. + pub fn get_bytes_avif(&self, speed: u8, quality: u8) -> Vec { + let img = helpers::dyn_image_from_raw(self); + + // AVIF supports alpha; preserve it. + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + + let mut buffer = Vec::new(); + let encoder = AvifEncoder::new_with_speed_quality(&mut buffer, speed, quality); + encoder.write_image(rgba.as_raw(), w, h, ExtendedColorType::Rgba8).unwrap(); buffer } diff --git a/crate/src/text.rs b/crate/src/text.rs index 2731cec..de2760a 100644 --- a/crate/src/text.rs +++ b/crate/src/text.rs @@ -4,11 +4,11 @@ use crate::iter::ImageIterator; use crate::{helpers, PhotonImage}; -use image::{DynamicImage, Rgba}; +use image::{DynamicImage, GrayImage, Luma, Rgba}; use imageproc::distance_transform::Norm; use imageproc::drawing::draw_text_mut; use imageproc::morphology::dilate_mut; -use rusttype::{Font, Scale}; +use ab_glyph::{FontArc, PxScale}; #[cfg(feature = "enable_wasm")] use wasm_bindgen::prelude::*; @@ -45,34 +45,19 @@ pub fn draw_text_with_border( ) { let mut image = helpers::dyn_image_from_raw(photon_img).to_rgba8(); - let mut image2: DynamicImage = - DynamicImage::new_luma8(image.width(), image.height()); + let mut mask: GrayImage = GrayImage::new(image.width(), image.height()); + let font_bytes: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf"); + let font = FontArc::try_from_slice(font_bytes).expect("invalid font bytes"); - let font = Vec::from(include_bytes!("../fonts/Roboto-Regular.ttf") as &[u8]); - let font = Font::try_from_bytes(&font).unwrap(); - let scale = Scale { - x: font_size * 1.0, - y: font_size, - }; - draw_text_mut( - &mut image2, - Rgba([255u8, 255u8, 255u8, 255u8]), - x, - y, - scale, - &font, - text, - ); - - let mut image2 = image2.to_luma8(); - dilate_mut(&mut image2, Norm::LInf, 4u8); + let scale: PxScale = PxScale::from(font_size); + draw_text_mut(&mut mask, Luma([255u8]), x, y, scale, &font, text); + dilate_mut(&mut mask, Norm::LInf, 4u8); - // Add a border to the text. - for (x, y) in ImageIterator::with_dimension(&image2.dimensions()) { - let pixval = 255 - image2.get_pixel(x, y)[0]; + for (px, py) in ImageIterator::with_dimension(&mask.dimensions()) { + let v = mask.get_pixel(px, py)[0]; + let pixval = 255u8 - v; if pixval != 255 { - let new_pix = Rgba([pixval, pixval, pixval, 255]); - image.put_pixel(x, y, new_pix); + image.put_pixel(px, py, Rgba([pixval, pixval, pixval, 255])); } } @@ -85,7 +70,8 @@ pub fn draw_text_with_border( &font, text, ); - let dynimage = image::DynamicImage::ImageRgba8(image); + + let dynimage = DynamicImage::ImageRgba8(image); photon_img.raw_pixels = dynimage.into_bytes(); } @@ -121,12 +107,10 @@ pub fn draw_text( ) { let mut image = helpers::dyn_image_from_raw(photon_img).to_rgba8(); - let font = Vec::from(include_bytes!("../fonts/Roboto-Regular.ttf") as &[u8]); - let font = Font::try_from_bytes(&font).unwrap(); - let scale = Scale { - x: font_size * 1.0, - y: font_size, - }; + let font_bytes: &[u8] = include_bytes!("../fonts/Roboto-Regular.ttf"); + let font = FontArc::try_from_slice(font_bytes).expect("invalid font bytes"); + + let scale: PxScale = PxScale::from(font_size); draw_text_mut( &mut image, @@ -137,6 +121,7 @@ pub fn draw_text( &font, text, ); - let dynimage = image::DynamicImage::ImageRgba8(image); + + let dynimage = DynamicImage::ImageRgba8(image); photon_img.raw_pixels = dynimage.into_bytes(); }