use winit::{
event_loop::ActiveEventLoop,
window::{CustomCursor, Icon},
};
use zng_task::channel::{IpcBytes, IpcBytesMut};
use zng_txt::{ToTxt as _, formatx};
use zng_unit::PxPoint;
use zng_view_api::{
Event,
image::{ImageEncodeId, ImageEncodeRequest, ImageEntryKind, ImageFormatCapability, ImageId},
};
use crate::{
AppEvent, AppEventSender,
image_cache::{FORMATS, Image, ImageCache, ImageData},
};
impl ImageCache {
pub fn encode(&mut self, ImageEncodeRequest { id, entries, format, .. }: ImageEncodeRequest) -> ImageEncodeId {
let task_id = self.encode_id_gen.incr();
let app_sender = self.app_sender.clone();
let img = self.get(id).cloned();
let entries: Vec<_> = entries.into_iter().map(|(id, kind)| (id, self.get(id).cloned(), kind)).collect();
rayon::spawn(move || Self::encode_impl(app_sender, format, task_id, id, img, entries));
task_id
}
fn encode_impl(
app_sender: AppEventSender,
format: zng_txt::Txt,
task_id: ImageEncodeId,
id: ImageId,
img: Option<Image>,
entries: Vec<(ImageId, Option<Image>, ImageEntryKind)>,
) {
let fmt = match FORMATS.iter().find(|f| f.matches(format.as_str())) {
Some(f) => {
if !f.capabilities.contains(ImageFormatCapability::ENCODE) {
Err("encoding not implemented")
} else if !entries.is_empty() && !f.capabilities.contains(ImageFormatCapability::ENCODE_ENTRIES) {
Err("multi entry encoding not implemented")
} else {
Ok(f)
}
}
None => Err("unknown format"),
};
let fmt = match fmt {
Ok(f) => f,
Err(e) => {
let error = formatx!("cannot encode `{id:?}` to `{format}`, {e}");
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncodeError { task: task_id, error }));
return;
}
};
if let Some(img) = img {
let mut entry_imgs = Vec::with_capacity(entries.len());
for (entry_id, img, kind) in entries {
match img {
Some(img) => {
entry_imgs.push((img.clone(), kind));
}
None => {
let error = formatx!(
"cannot encode `{id:?}` to `{}`, entry image ({entry_id:?}) not found",
fmt.display_name
);
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncodeError { task: task_id, error }));
return;
}
}
}
let fmt = image::ImageFormat::from_extension(fmt.file_extensions_iter().next().unwrap()).unwrap();
debug_assert!(fmt.can_write());
let img = img.clone();
let mut data = IpcBytes::new_writer_blocking();
match img.encode(entry_imgs, fmt, &mut data) {
Ok(_) => match data.finish() {
Ok(data) => {
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncoded { task: task_id, data }));
}
Err(e) => {
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncodeError {
task: task_id,
error: e.to_txt(),
}));
}
},
Err(e) => {
let error = formatx!("failed to encode `{id:?}` to `{format}`, {e}");
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncodeError { task: task_id, error }));
}
}
} else {
let error = formatx!("cannot encode `{id:?}` to `{}`, image not found", fmt.display_name);
let _ = app_sender.send(AppEvent::Notify(Event::ImageEncodeError { task: task_id, error }));
}
}
}
impl Image {
pub fn icon(&self) -> Option<Icon> {
let (size, pixels) = match &*self.0 {
ImageData::RawData { size, pixels, .. } => (size, pixels),
ImageData::NativeTexture { .. } => unreachable!(),
};
let width = size.width.0 as u32;
let height = size.height.0 as u32;
if width == 0 || height == 0 || self.0.is_mask() {
None
} else {
let r = if width > 255 || height > 255 {
let mut buf = pixels[..].to_vec();
bgra_pre_mul_to_rgba(&mut buf, self.is_opaque());
let img = image::ImageBuffer::from_raw(width, height, buf).unwrap();
let img = image::DynamicImage::ImageRgba8(img);
let img = img.resize(255, 255, image::imageops::FilterType::Lanczos3);
use image::GenericImageView;
let (width, height) = img.dimensions();
let buf = img.into_rgba8().into_raw();
winit::window::Icon::from_rgba(buf, width, height)
} else {
let mut buf = pixels[..].to_vec();
bgra_pre_mul_to_rgba(&mut buf, self.is_opaque());
winit::window::Icon::from_rgba(buf, width, height)
};
match r {
Ok(i) => Some(i),
Err(e) => {
tracing::error!("failed to convert image to custom icon, {e}");
None
}
}
}
}
pub fn cursor(&self, hotspot: PxPoint, event_loop: &ActiveEventLoop) -> Option<CustomCursor> {
let (size, pixels) = match &*self.0 {
ImageData::RawData { size, pixels, .. } => (size, pixels),
ImageData::NativeTexture { .. } => unreachable!(),
};
let width: u16 = size.width.0.try_into().ok()?;
let height: u16 = size.height.0.try_into().ok()?;
let hotspot_x: u16 = hotspot.x.0.try_into().ok()?;
let hotspot_y: u16 = hotspot.y.0.try_into().ok()?;
if width == 0 || height == 0 || hotspot_x > width || hotspot_y > height || self.0.is_mask() {
None
} else {
let mut buf = pixels[..].to_vec();
bgra_pre_mul_to_rgba(&mut buf, self.is_opaque());
match CustomCursor::from_rgba(buf, width, height, hotspot_x, hotspot_y) {
Ok(c) => Some(event_loop.create_custom_cursor(c)),
Err(e) => {
tracing::error!("failed to convert image to custom cursor, {e}");
None
}
}
}
}
pub fn encode(
&self,
entries: Vec<(Image, ImageEntryKind)>,
format: image::ImageFormat,
buffer: &mut dyn EncodeBuffer,
) -> image::ImageResult<()> {
let (size, pixels, density) = match &*self.0 {
ImageData::RawData { size, pixels, density, .. } => (size, pixels, density),
ImageData::NativeTexture { .. } => unreachable!(),
};
if size.width <= 0 || size.height <= 0 {
return Err(image::ImageError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot encode zero sized image",
)));
}
if !entries.is_empty() {
match format {
#[cfg(feature = "image_ico")]
image::ImageFormat::Ico => {}
#[cfg(feature = "image_tiff")]
image::ImageFormat::Tiff => {}
f => {
return Err(image::ImageError::Encoding(image::error::EncodingError::new(
image::error::ImageFormatHint::Exact(f),
"cannot encode entries for format",
)));
}
};
}
let is_mask = self.0.is_mask();
let mut buf = IpcBytesMut::from_slice_blocking(&pixels[..])?;
if !is_mask {
bgra_pre_mul_to_rgba(&mut buf, self.0.is_opaque());
}
match format {
#[cfg(feature = "image_jpeg")]
image::ImageFormat::Jpeg => {
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let mut jpg = image::codecs::jpeg::JpegEncoder::new(buffer);
if let Some(density) = density {
jpg.set_pixel_density(image::codecs::jpeg::PixelDensity {
density: (density.height.ppi() as u16, density.height.ppi() as u16),
unit: image::codecs::jpeg::PixelDensityUnit::Inches,
});
}
if is_mask {
jpg.encode(&buf, width, height, image::ColorType::L8.into())?;
} else {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
jpg.encode(&buf, width, height, image::ColorType::Rgb8.into())?;
}
Ok(())
}
#[cfg(feature = "image_png")]
image::ImageFormat::Png => {
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
let mut img = png::Encoder::new(buffer, width, height);
img.set_compression(png::Compression::Fast); img.set_depth(png::BitDepth::Eight);
img.set_color(if is_mask {
png::ColorType::Grayscale
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
png::ColorType::Rgb
} else {
png::ColorType::Rgba
});
if let Some(density) = density {
img.set_pixel_dims(Some(png::PixelDimensions {
xppu: density.width.ppm() as _,
yppu: density.height.ppm() as _,
unit: png::Unit::Meter,
}));
}
let mut img = img.write_header().map_err(|e| {
image::ImageError::Encoding(image::error::EncodingError::new(
image::error::ImageFormatHint::Exact(image::ImageFormat::Png),
e,
))
})?;
img.write_image_data(&buf).map_err(|e| {
image::ImageError::Encoding(image::error::EncodingError::new(
image::error::ImageFormatHint::Exact(image::ImageFormat::Png),
e,
))
})?;
Ok(())
}
#[cfg(feature = "image_gif")]
image::ImageFormat::Gif => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
if is_mask {
let mut expanded = IpcBytesMut::new_blocking(buf.len() * 3)?;
for (p, &a) in expanded.chunks_exact_mut(3).zip(&buf[..]) {
p[0] = a;
p[1] = a;
p[2] = a;
}
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
}
let mut img = image::codecs::gif::GifEncoder::new(buffer);
img.encode(
&buf,
width,
height,
if is_opaque || is_mask {
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
},
)?;
Ok(())
}
#[cfg(feature = "image_webp")]
image::ImageFormat::WebP => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
let ct = if is_mask {
image::ColorType::L8.into()
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
};
let img = image::codecs::webp::WebPEncoder::new_lossless(buffer);
img.encode(&buf, width, height, ct)?;
Ok(())
}
#[cfg(feature = "image_pnm")]
image::ImageFormat::Pnm => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
let ct = if is_mask {
image::ColorType::L8.into()
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
};
let mut img = image::codecs::pnm::PnmEncoder::new(buffer);
img.encode(&buf[..], width, height, ct)?;
Ok(())
}
#[cfg(feature = "image_tiff")]
image::ImageFormat::Tiff => {
let _ = density;
let is_opaque = self.0.is_opaque();
Self::encode_tiff(*size, is_mask, pixels, is_opaque, entries, buffer).map_err(|e| {
image::ImageError::Encoding(image::error::EncodingError::new(image::error::ImageFormatHint::Exact(format), e))
})?;
Ok(())
}
#[cfg(feature = "image_tga")]
image::ImageFormat::Tga => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
let ct = if is_mask {
image::ColorType::L8.into()
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
};
let img = image::codecs::tga::TgaEncoder::new(buffer);
img.encode(&buf, width, height, ct)?;
Ok(())
}
#[cfg(feature = "image_bmp")]
image::ImageFormat::Bmp => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
let ct = if is_mask {
image::ColorType::L8.into()
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
};
struct SizedProxy<'a>(&'a mut dyn EncodeBuffer);
impl<'a> std::io::Write for SizedProxy<'a> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}
let mut buffer = SizedProxy(buffer);
let mut img = image::codecs::bmp::BmpEncoder::new(&mut buffer);
img.encode(&buf, width, height, ct)?;
Ok(())
}
#[cfg(feature = "image_ico")]
image::ImageFormat::Ico => {
let _ = density;
let is_opaque = self.0.is_opaque();
Self::encode_ico(*size, is_mask, pixels, is_opaque, entries, buffer).map_err(|e| {
image::ImageError::Encoding(image::error::EncodingError::new(image::error::ImageFormatHint::Exact(format), e))
})?;
Ok(())
}
#[cfg(feature = "image_hdr")]
image::ImageFormat::Hdr => {
use image::ImageEncoder as _;
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
const F: f32 = 1.0 / 255.0;
let mut rgb = zng_task::channel::IpcBytesMutCast::<[f32; 3]>::new_blocking(width as usize * height as usize)?;
if is_mask {
for (c, a) in rgb.iter_mut().zip(buf.iter()) {
let a = *a as f32 * F;
c[0] = a;
c[1] = a;
c[2] = a;
}
} else {
for (c32, c8) in rgb.iter_mut().zip(buf.chunks_exact(4)) {
for (c32, c8) in c32.iter_mut().zip(c8.iter()) {
*c32 = *c8 as f32 * F;
}
}
}
let img = image::codecs::hdr::HdrEncoder::new(buffer);
img.write_image(rgb.as_bytes(), width, height, image::ColorType::Rgb32F.into())?;
Ok(())
}
#[cfg(feature = "image_exr")]
image::ImageFormat::OpenExr => {
use image::ImageEncoder as _;
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
const F: f32 = 1.0 / 255.0;
let img = image::codecs::openexr::OpenExrEncoder::new(buffer);
let ct = if is_mask {
let mut rgb = zng_task::channel::IpcBytesMutCast::<[f32; 3]>::new_blocking(width as usize * height as usize)?;
for (c, a) in rgb.iter_mut().zip(buf.iter()) {
let a = *a as f32 * F;
c[0] = a;
c[1] = a;
c[2] = a;
}
buf = rgb.into_inner();
image::ColorType::Rgb32F.into()
} else if is_opaque {
let mut rgb = zng_task::channel::IpcBytesMutCast::<[f32; 3]>::new_blocking(width as usize * height as usize)?;
for (c32, c8) in rgb.iter_mut().zip(buf.chunks_exact(4)) {
for (c, a) in c32.iter_mut().zip(c8.iter()) {
*c = *a as f32 * F;
}
}
buf = rgb.into_inner();
image::ColorType::Rgb32F.into()
} else {
let mut rgba = zng_task::channel::IpcBytesMutCast::<[f32; 4]>::new_blocking(width as usize * height as usize)?;
for (c32, c8) in rgba.iter_mut().zip(buf.chunks_exact(4)) {
for (c32, c8) in c32.iter_mut().zip(c8.iter()) {
*c32 = *c8 as f32 * F;
}
}
buf = rgba.into_inner();
image::ColorType::Rgba32F.into()
};
img.write_image(&buf, width, height, ct)?;
Ok(())
}
#[cfg(feature = "image_ff")]
image::ImageFormat::Farbfeld => {
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
const F: u16 = 257;
let mut rgba = zng_task::channel::IpcBytesMutCast::<[u16; 4]>::new_blocking(width as usize * height as usize)?;
if is_mask {
for (c, a) in rgba.iter_mut().zip(buf.iter()) {
let a = *a as u16 * 257;
c[0] = a;
c[1] = a;
c[2] = a;
c[3] = u16::MAX;
}
} else {
for (c16, c8) in rgba.iter_mut().zip(buf.chunks_exact(4)) {
for (c16, c8) in c16.iter_mut().zip(c8.iter()) {
*c16 = *c8 as u16 * F;
}
}
}
let img = image::codecs::farbfeld::FarbfeldEncoder::new(buffer);
img.encode(rgba.as_bytes(), width, height)?;
Ok(())
}
#[cfg(feature = "image_qoi")]
image::ImageFormat::Qoi => {
use image::ImageEncoder as _;
let _ = density;
let width = size.width.0 as u32;
let height = size.height.0 as u32;
let is_opaque = self.0.is_opaque();
if is_mask {
let mut expanded = IpcBytesMut::new_blocking(buf.len() * 3)?;
for (p, &a) in expanded.chunks_exact_mut(3).zip(&buf[..]) {
p[0] = a;
p[1] = a;
p[2] = a;
}
} else if is_opaque {
buf.reduce_in_place(|[r, g, b, _]| [r, g, b]);
};
let img = image::codecs::qoi::QoiEncoder::new(buffer);
img.write_image(
&buf,
width,
height,
if is_opaque || is_mask {
image::ColorType::Rgb8.into()
} else {
image::ColorType::Rgba8.into()
},
)?;
Ok(())
}
f => {
let _ = density;
let _ = buffer;
Err(image::ImageError::Encoding(image::error::EncodingError::new(
image::error::ImageFormatHint::Exact(f),
"cannot encode format",
)))
}
}
}
#[cfg(feature = "image_ico")]
fn encode_ico(
size: zng_unit::PxSize,
is_mask: bool,
pixels: &IpcBytes,
is_opaque: bool,
entries: Vec<(Image, ImageEntryKind)>,
buffer: &mut dyn EncodeBuffer,
) -> std::io::Result<()> {
let mut ico = ico::IconDir::new(ico::ResourceType::Icon);
fn to_ico_img(size: zng_unit::PxSize, pixels: &IpcBytes, is_mask: bool, is_opaque: bool) -> ico::IconImage {
let rgba = if is_mask {
let mut v = Vec::with_capacity(pixels.len() * 4);
for &p in pixels.iter() {
v.extend_from_slice(&[p, p, p, 255]);
}
v
} else {
let mut p = pixels.to_vec();
bgra_pre_mul_to_rgba(&mut p, is_opaque);
p
};
ico::IconImage::from_rgba_data(size.width.0 as _, size.height.0 as _, rgba)
}
ico.add_entry(ico::IconDirEntry::encode(&to_ico_img(size, pixels, is_mask, is_opaque))?);
for (entry, kind) in entries {
if !matches!(kind, ImageEntryKind::Reduced { .. }) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot encode `Reduced` image entries",
));
}
let (size, pixels, is_opaque) = match &*entry.0 {
ImageData::RawData {
size, pixels, is_opaque, ..
} => (size, pixels, is_opaque),
ImageData::NativeTexture { .. } => unreachable!(),
};
if size.width <= 0 || size.height <= 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot encode zero sized image entry",
));
}
ico.add_entry(ico::IconDirEntry::encode(&to_ico_img(*size, pixels, is_mask, *is_opaque))?);
}
ico.write(buffer)
}
#[cfg(feature = "image_tiff")]
fn encode_tiff(
size: zng_unit::PxSize,
is_mask: bool,
pixels: &IpcBytes,
is_opaque: bool,
entries: Vec<(Image, ImageEntryKind)>,
buffer: &mut dyn EncodeBuffer,
) -> tiff::TiffResult<()> {
let mut tiff = tiff::encoder::TiffEncoder::new(buffer)?
.with_compression(tiff::encoder::Compression::Lzw)
.with_predictor(tiff::encoder::Predictor::Horizontal);
let total_pages = (1 + entries.iter().filter(|t| matches!(t.1, ImageEntryKind::Page)).count()) as u16;
if is_mask {
let mut img = tiff.new_image::<tiff::encoder::colortype::Gray8>(size.width.0 as _, size.height.0 as _)?;
img.encoder().write_tag(tiff::tags::Tag::NewSubfileType, 0x0u32)?;
img.encoder().write_tag(tiff::tags::Tag::Unknown(297), &[0, total_pages][..])?;
img.write_data(&pixels[..])?;
} else {
let mut buf = pixels.to_vec();
bgra_pre_mul_to_rgba(&mut buf, is_opaque);
let mut img = tiff.new_image::<tiff::encoder::colortype::RGBA8>(size.width.0 as _, size.height.0 as _)?;
img.encoder().write_tag(tiff::tags::Tag::NewSubfileType, 0x0u32)?;
img.encoder().write_tag(tiff::tags::Tag::Unknown(297), &[0, total_pages][..])?;
img.write_data(&buf[..])?;
}
let mut page_n = 0;
for (entry, kind) in entries {
let (size, pixels, is_opaque) = match &*entry.0 {
ImageData::RawData {
size, pixels, is_opaque, ..
} => (size, pixels, is_opaque),
ImageData::NativeTexture { .. } => unreachable!(),
};
let is_mask = entry.0.is_mask();
let subfile_type = match kind {
ImageEntryKind::Page => {
page_n += 1;
0x02u32
}
ImageEntryKind::Reduced { .. } => 0x01,
_ => 0x0u32,
};
if is_mask {
let mut img = tiff.new_image::<tiff::encoder::colortype::Gray8>(size.width.0 as _, size.height.0 as _)?;
img.encoder().write_tag(tiff::tags::Tag::NewSubfileType, subfile_type)?;
img.encoder().write_tag(tiff::tags::Tag::Unknown(297), &[page_n, total_pages][..])?;
img.write_data(&pixels[..])?;
} else {
let mut buf = pixels.to_vec();
bgra_pre_mul_to_rgba(&mut buf, *is_opaque);
let mut img = tiff.new_image::<tiff::encoder::colortype::RGBA8>(size.width.0 as _, size.height.0 as _)?;
img.encoder().write_tag(tiff::tags::Tag::NewSubfileType, subfile_type)?;
img.encoder().write_tag(tiff::tags::Tag::Unknown(297), &[page_n, total_pages][..])?;
img.write_data(&buf[..])?;
}
}
Ok(())
}
}
pub(crate) trait EncodeBuffer: std::io::Write + std::io::Seek {}
impl<W: std::io::Write + std::io::Seek> EncodeBuffer for W {}
fn bgra_pre_mul_to_rgba(buf: &mut [u8], is_opaque: bool) {
if is_opaque {
buf.chunks_exact_mut(4).for_each(|c| c.swap(0, 2));
} else {
buf.chunks_exact_mut(4).for_each(|c| {
let alpha = c[3];
let is_not_zero = (alpha > 0) as u8 as f32;
let divisor = (alpha as f32) + (1.0 - is_not_zero);
let scale = (255.0 / divisor) * is_not_zero;
let b = c[0] as f32 * scale;
let g = c[1] as f32 * scale;
let r = c[2] as f32 * scale;
c[0] = r.min(255.0).round() as u8;
c[1] = g.min(255.0).round() as u8;
c[2] = b.min(255.0).round() as u8;
});
}
}