use std::{
any::Any,
env, fmt, fs, io, mem, ops,
path::{Path, PathBuf},
sync::Arc,
};
use parking_lot::Mutex;
use zng_app::{
view_process::{EncodeError, VIEW_PROCESS, ViewImageHandle, ViewRenderer},
widget::node::UiNode,
window::WindowId,
};
use zng_color::{
Hsla, Rgba,
gradient::{ExtendMode, GradientStops},
};
use zng_layout::{
context::{LAYOUT, LayoutMetrics, LayoutPassId},
unit::{ByteLength, ByteUnits, FactorUnits as _, LayoutAxis, Px, PxDensity2d, PxLine, PxPoint, PxRect, PxSize, about_eq},
};
use zng_task::{
self as task,
channel::{IpcBytes, IpcBytesMut},
};
use zng_txt::Txt;
use zng_var::{Var, VarEq, animation::Transitionable, impl_from_and_into_var};
use zng_view_api::{
image::{ImageDecoded, ImageEncodeRequest, ImageEntryMetadata, ImageTextureId},
window::RenderMode,
};
use crate::{IMAGES_SV, ImageRenderWindowRoot};
pub use zng_view_api::image::{
ColorType, ImageDataFormat, ImageDownscaleMode, ImageEntriesMode, ImageEntryKind, ImageFormat, ImageFormatCapability, ImageMaskMode,
PartialImageKind,
};
pub trait ImagesExtension: Send + Sync + Any {
fn image(&mut self, limits: &ImageLimits, source: &mut ImageSource, options: &mut ImageOptions) {
let _ = (limits, source, options);
}
#[allow(clippy::too_many_arguments)]
fn image_data(
&mut self,
max_decoded_len: ByteLength,
key: &ImageHash,
data: &IpcBytes,
format: &ImageDataFormat,
options: &ImageOptions,
) -> Option<ImageVar> {
let _ = (max_decoded_len, key, data, format, options);
None
}
fn remove(&mut self, key: &mut ImageHash, purge: &mut bool) -> bool {
let _ = (key, purge);
true
}
fn clear(&mut self, purge: bool) {
let _ = purge;
}
fn available_formats(&self, formats: &mut Vec<ImageFormat>) {
let _ = formats;
}
}
pub type ImageVar = Var<ImageEntry>;
#[derive(Default, Debug)]
struct ImgMut {
render_ids: Vec<RenderImage>,
}
#[derive(Debug, Clone)]
pub struct ImageEntry {
pub(crate) cache_key: Option<ImageHash>,
pub(crate) handle: ViewImageHandle,
pub(crate) data: ImageDecoded,
entries: Vec<VarEq<ImageEntry>>,
error: Txt,
img_mut: Arc<Mutex<ImgMut>>,
}
impl PartialEq for ImageEntry {
fn eq(&self, other: &Self) -> bool {
self.handle == other.handle
&& self.cache_key == other.cache_key
&& self.error == other.error
&& self.data == other.data
&& self.entries == other.entries
}
}
impl ImageEntry {
pub fn new_loading() -> Self {
Self::new_error(Txt::from_static(""))
}
pub fn new_error(error: Txt) -> Self {
let mut s = Self::new(None, ViewImageHandle::dummy(), ImageDecoded::default());
s.error = error;
s
}
pub(crate) fn new(cache_key: Option<ImageHash>, handle: ViewImageHandle, data: ImageDecoded) -> Self {
Self {
cache_key,
handle,
data,
entries: vec![],
error: Txt::from_static(""),
img_mut: Arc::default(),
}
}
pub fn is_loading(&self) -> bool {
self.error.is_empty() && (self.data.pixels.is_empty() || self.data.partial.is_some())
}
pub fn is_loaded(&self) -> bool {
!self.is_loading()
}
pub fn is_error(&self) -> bool {
!self.error.is_empty()
}
pub fn error(&self) -> Option<Txt> {
if self.error.is_empty() { None } else { Some(self.error.clone()) }
}
pub fn size(&self) -> PxSize {
self.data.meta.size
}
pub fn partial_size(&self) -> Option<PxSize> {
match self.data.partial.as_ref()? {
PartialImageKind::Placeholder { pixel_size } => Some(*pixel_size),
PartialImageKind::Rows { height, .. } => Some(PxSize::new(self.data.meta.size.width, *height)),
_ => None,
}
}
pub fn partial_kind(&self) -> Option<PartialImageKind> {
self.data.partial.clone()
}
pub fn density(&self) -> Option<PxDensity2d> {
self.data.meta.density
}
pub fn original_color_type(&self) -> ColorType {
self.data.meta.original_color_type.clone()
}
pub fn color_type(&self) -> ColorType {
if self.is_mask() { ColorType::A8 } else { ColorType::BGRA8 }
}
pub fn is_opaque(&self) -> bool {
self.data.is_opaque
}
pub fn is_mask(&self) -> bool {
self.data.meta.is_mask
}
pub fn has_entries(&self) -> bool {
!self.entries.is_empty()
}
pub fn entries(&self) -> Vec<ImageVar> {
self.entries.iter().map(|e| e.read_only()).collect()
}
pub fn flat_entries(&self) -> Var<Vec<(VarEq<ImageEntry>, usize)>> {
let update_signal = zng_var::var(());
let mut out = vec![];
let mut update_handles = vec![];
self.flat_entries_init(&mut out, update_signal.clone(), &mut update_handles);
let out = zng_var::var(out);
let self_ = self.clone();
let signal_weak = update_signal.downgrade();
update_signal
.bind_modify(&out, move |_, out| {
out.clear();
update_handles.clear();
self_.flat_entries_init(&mut *out, signal_weak.upgrade().unwrap(), &mut update_handles);
})
.perm();
out.hold(update_signal).perm();
out.read_only()
}
fn flat_entries_init(&self, out: &mut Vec<(VarEq<ImageEntry>, usize)>, update_signal: Var<()>, handles: &mut Vec<zng_var::VarHandle>) {
for entry in self.entries.iter() {
Self::flat_entries_recursive_init(entry.clone(), out, update_signal.clone(), handles);
}
}
fn flat_entries_recursive_init(
img: VarEq<ImageEntry>,
out: &mut Vec<(VarEq<ImageEntry>, usize)>,
signal: Var<()>,
handles: &mut Vec<zng_var::VarHandle>,
) {
handles.push(img.hook(zng_clone_move::clmv!(signal, |_| {
signal.modify(|a| {
let _ = a.value_mut();
});
true
})));
let i = out.len();
out.push((img.clone(), 0));
img.with(move |img| {
for entry in img.entries.iter() {
Self::flat_entries_recursive_init(entry.clone(), out, signal.clone(), handles);
}
let len = out.len() - i;
out[i].1 = len;
});
}
pub fn entry_kind(&self) -> ImageEntryKind {
match &self.data.meta.parent {
Some(p) => p.kind.clone(),
None => ImageEntryKind::Page,
}
}
pub fn entry_index(&self) -> usize {
match &self.data.meta.parent {
Some(p) => p.index,
None => 0,
}
}
pub fn with_best_reduce<R>(&self, size: PxSize, visit: impl FnOnce(&ImageEntry) -> R) -> Option<R> {
fn cmp(target_size: PxSize, a: PxSize, b: PxSize) -> std::cmp::Ordering {
let target_ratio = target_size.width.0 as f32 / target_size.height.0 as f32;
let a_ratio = a.width.0 as f32 / b.height.0 as f32;
let b_ratio = b.width.0 as f32 / b.height.0 as f32;
let a_distortion = (target_ratio - a_ratio).abs();
let b_distortion = (target_ratio - b_ratio).abs();
if !about_eq(a_distortion, b_distortion, 0.01) && a_distortion < b_distortion {
return std::cmp::Ordering::Less;
}
let a_dist = a - target_size;
let b_dist = b - target_size;
if a_dist.width < Px(0) || a_dist.height < Px(0) {
if b_dist.width < Px(0) || b_dist.height < Px(0) {
a_dist.width.abs().cmp(&b_dist.width.abs())
} else {
std::cmp::Ordering::Greater
}
} else if b_dist.width < Px(0) || b_dist.height < Px(0) {
std::cmp::Ordering::Less
} else {
a_dist.width.cmp(&b_dist.width)
}
}
let mut best_i = usize::MAX;
let mut best_size = PxSize::zero();
if self.is_loaded() {
best_i = self.entries.len();
best_size = self.size();
}
for (i, entry) in self.entries.iter().enumerate() {
entry.with(|e| {
if e.is_loaded() {
let entry_size = e.size();
if cmp(size, entry_size, best_size).is_lt() {
best_i = i;
best_size = entry_size;
}
}
})
}
if best_i == usize::MAX {
None
} else if best_i == self.entries.len() {
Some(visit(self))
} else {
Some(self.entries[best_i].with(visit))
}
}
pub fn view_handle(&self) -> &ViewImageHandle {
&self.handle
}
pub fn layout_size(&self, ctx: &LayoutMetrics) -> PxSize {
self.calc_size(ctx, PxDensity2d::splat(ctx.screen_density()), false)
}
pub fn calc_size(&self, ctx: &LayoutMetrics, fallback_density: PxDensity2d, ignore_image_density: bool) -> PxSize {
let dpi = if ignore_image_density {
fallback_density
} else {
self.density().unwrap_or(fallback_density)
};
let s_density = ctx.screen_density();
let mut size = self.size();
let fct = ctx.scale_factor().0;
size.width *= (s_density.ppcm() / dpi.width.ppcm()) * fct;
size.height *= (s_density.ppcm() / dpi.height.ppcm()) * fct;
size
}
pub fn pixels(&self) -> Option<IpcBytes> {
if self.is_loaded() { Some(self.data.pixels.clone()) } else { None }
}
pub fn partial_pixels(&self) -> Option<IpcBytes> {
if self.is_loading() && self.data.partial.is_some() {
Some(self.data.pixels.clone())
} else {
None
}
}
fn actual_pixels_and_size(&self) -> Option<(PxSize, IpcBytes)> {
match (self.partial_pixels(), self.partial_size()) {
(Some(b), Some(s)) => Some((s, b)),
_ => Some((self.size(), self.pixels()?)),
}
}
pub fn copy_pixels(&self, rect: PxRect) -> Option<(PxRect, IpcBytesMut)> {
self.actual_pixels_and_size().and_then(|(size, pixels)| {
let area = PxRect::from_size(size).intersection(&rect).unwrap_or_default();
if area.size.width.0 == 0 || area.size.height.0 == 0 {
Some((area, IpcBytes::new_mut_blocking(0).unwrap()))
} else {
let x = area.origin.x.0 as usize;
let y = area.origin.y.0 as usize;
let width = area.size.width.0 as usize;
let height = area.size.height.0 as usize;
let pixel = if self.is_mask() { 1 } else { 4 };
let mut bytes = IpcBytes::new_mut_blocking(width * height * pixel).ok()?;
let mut write = &mut bytes[..];
let row_stride = self.size().width.0 as usize * pixel;
for l in y..y + height {
let line_start = l * row_stride + x * pixel;
let line_end = line_start + width * pixel;
let line = &pixels[line_start..line_end];
write[..line.len()].copy_from_slice(line);
write = &mut write[line.len()..];
}
Some((area, bytes))
}
})
}
pub async fn encode(&self, format: Txt) -> std::result::Result<IpcBytes, EncodeError> {
self.encode_with_entries(&[], format).await
}
pub async fn encode_with_entries(
&self,
entries: &[(ImageEntry, ImageEntryKind)],
format: Txt,
) -> std::result::Result<IpcBytes, EncodeError> {
if self.is_loading() {
return Err(EncodeError::Loading);
} else if let Some(e) = self.error() {
return Err(e.into());
} else if self.handle.is_dummy() {
return Err(EncodeError::Dummy);
}
let mut r = ImageEncodeRequest::new(self.handle.image_id(), format);
r.entries = entries.iter().map(|(img, kind)| (img.handle.image_id(), kind.clone())).collect();
match VIEW_PROCESS.encode_image(r).recv().await {
Ok(r) => r,
Err(_) => Err(EncodeError::Disconnected),
}
}
pub async fn save(&self, path: impl Into<PathBuf>) -> io::Result<()> {
let path = path.into();
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
self.save_impl(&[], Txt::from_str(ext), path).await
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"could not determinate image format from path extension",
))
}
}
pub async fn save_with_format(&self, format: impl Into<Txt>, path: impl Into<PathBuf>) -> io::Result<()> {
self.save_impl(&[], format.into(), path.into()).await
}
pub async fn save_with_entries(
&self,
entries: &[(ImageEntry, ImageEntryKind)],
format: impl Into<Txt>,
path: impl Into<PathBuf>,
) -> io::Result<()> {
self.save_impl(entries, format.into(), path.into()).await
}
async fn save_impl(&self, entries: &[(ImageEntry, ImageEntryKind)], format: Txt, path: PathBuf) -> io::Result<()> {
let data = self.encode_with_entries(entries, format).await.map_err(io::Error::other)?;
task::wait(move || fs::write(path, &data[..])).await
}
pub fn insert_entry(&mut self, entry: ImageVar) {
let id = self.handle.image_id();
let (i, p) = entry.with(|i| (i.entry_index(), i.data.meta.parent.clone()));
let i = self
.entries
.iter()
.position(|v| {
let entry_i = v.with(|i| i.entry_index());
entry_i > i
})
.unwrap_or(self.entries.len());
if let Some(p) = &p {
if p.parent != id {
tracing::warn!("replacing entry parent from {:?} tp {:?}", p.parent, id);
entry.modify(move |e| {
if let Some(p) = &mut e.data.meta.parent {
p.parent = id;
} else {
e.data.meta.parent = Some(ImageEntryMetadata::new(id, i, ImageEntryKind::Page));
}
});
}
} else {
entry.modify(move |e| e.data.meta.parent = Some(ImageEntryMetadata::new(id, i, ImageEntryKind::Page)));
}
self.entries.insert(i, VarEq(entry));
}
}
impl zng_app::render::Img for ImageEntry {
fn renderer_id(&self, renderer: &ViewRenderer) -> ImageTextureId {
if self.is_loaded() {
let mut img = self.img_mut.lock();
let rms = &mut img.render_ids;
if let Some(rm) = rms.iter().find(|k| &k.renderer == renderer) {
return rm.image_id;
}
let key = match renderer.use_image(&self.handle) {
Ok(k) => {
if k == ImageTextureId::INVALID {
tracing::error!("received INVALID from `use_image`");
return k;
}
k
}
Err(_) => {
tracing::debug!("respawned `add_image`, will return INVALID");
return ImageTextureId::INVALID;
}
};
rms.push(RenderImage {
image_id: key,
renderer: renderer.clone(),
});
key
} else {
ImageTextureId::INVALID
}
}
fn size(&self) -> PxSize {
self.size()
}
}
struct RenderImage {
image_id: ImageTextureId,
renderer: ViewRenderer,
}
impl Drop for RenderImage {
fn drop(&mut self) {
let _ = self.renderer.delete_image_use(self.image_id);
}
}
impl fmt::Debug for RenderImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.image_id, f)
}
}
#[derive(Clone, Copy)]
pub struct ImageHash([u8; 32]);
impl ImageHash {
pub fn compute(data: &[u8]) -> Self {
let mut h = Self::hasher();
h.update(data);
h.finish()
}
pub fn hasher() -> ImageHasher {
ImageHasher::default()
}
}
impl fmt::Debug for ImageHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_tuple("ImageHash").field(&self.0).finish()
} else {
use base64::*;
write!(f, "{}", base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.0))
}
}
}
impl fmt::Display for ImageHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl std::hash::Hash for ImageHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let h64 = [
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7],
];
state.write_u64(u64::from_ne_bytes(h64))
}
}
impl PartialEq for ImageHash {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for ImageHash {}
pub struct ImageHasher(sha2::Sha512_256);
impl Default for ImageHasher {
fn default() -> Self {
use sha2::Digest;
Self(sha2::Sha512_256::new())
}
}
impl ImageHasher {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, data: &[u8]) {
use sha2::Digest;
const NUM_SAMPLES: usize = 1000;
const SAMPLE_CHUNK_SIZE: usize = 1024;
let total_size = data.len();
if total_size == 0 {
return;
}
if total_size < 1000 * 1000 * 4 {
return self.0.update(data);
}
let step_size = total_size.checked_div(NUM_SAMPLES).unwrap_or(total_size);
for n in 0..NUM_SAMPLES {
let start_index = n * step_size;
if start_index >= total_size {
break;
}
let end_index = (start_index + SAMPLE_CHUNK_SIZE).min(total_size);
let s = &data[start_index..end_index];
self.0.update(s);
}
}
pub fn finish(self) -> ImageHash {
use sha2::Digest;
ImageHash(self.0.finalize().as_slice().try_into().unwrap())
}
}
impl std::hash::Hasher for ImageHasher {
fn finish(&self) -> u64 {
tracing::warn!("Hasher::finish called for ImageHasher");
use sha2::Digest;
let hash = self.0.clone().finalize();
u64::from_le_bytes(hash[..8].try_into().unwrap())
}
fn write(&mut self, bytes: &[u8]) {
self.update(bytes);
}
}
pub(crate) type RenderFn = Arc<Box<dyn Fn(&ImageRenderArgs) -> Box<dyn ImageRenderWindowRoot> + Send + Sync>>;
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct ImageRenderArgs {
pub parent: Option<WindowId>,
}
impl ImageRenderArgs {
pub fn new(parent: WindowId) -> Self {
Self { parent: Some(parent) }
}
}
#[derive(Clone)]
#[non_exhaustive]
pub enum ImageSource {
Read(PathBuf),
#[cfg(feature = "http")]
Download(zng_task::http::Uri, Option<Txt>),
Data(ImageHash, IpcBytes, ImageDataFormat),
Render(RenderFn, Option<ImageRenderArgs>),
Image(ImageVar),
}
impl ImageSource {
pub fn from_data(data: IpcBytes, format: ImageDataFormat) -> Self {
let mut hasher = ImageHasher::default();
hasher.update(&data[..]);
let hash = hasher.finish();
Self::Data(hash, data, format)
}
pub fn hash128(&self, options: &ImageOptions) -> Option<ImageHash> {
match self {
ImageSource::Read(p) => Some(Self::hash128_read(p, options)),
#[cfg(feature = "http")]
ImageSource::Download(u, a) => Some(Self::hash128_download(u, a, options)),
ImageSource::Data(h, _, _) => Some(Self::hash128_data(*h, options)),
ImageSource::Render(rfn, args) => Some(Self::hash128_render(rfn, args, options)),
ImageSource::Image(_) => None,
}
}
pub fn hash128_data(data_hash: ImageHash, options: &ImageOptions) -> ImageHash {
if options.downscale.is_some() || options.mask.is_some() || !options.entries.is_empty() {
use std::hash::Hash;
let mut h = ImageHash::hasher();
data_hash.0.hash(&mut h);
options.downscale.hash(&mut h);
options.mask.hash(&mut h);
options.entries.hash(&mut h);
h.finish()
} else {
data_hash
}
}
pub fn hash128_read(path: &Path, options: &ImageOptions) -> ImageHash {
use std::hash::Hash;
let mut h = ImageHash::hasher();
0u8.hash(&mut h);
path.hash(&mut h);
options.downscale.hash(&mut h);
options.mask.hash(&mut h);
options.entries.hash(&mut h);
h.finish()
}
#[cfg(feature = "http")]
pub fn hash128_download(uri: &zng_task::http::Uri, accept: &Option<Txt>, options: &ImageOptions) -> ImageHash {
use std::hash::Hash;
let mut h = ImageHash::hasher();
1u8.hash(&mut h);
uri.hash(&mut h);
accept.hash(&mut h);
options.downscale.hash(&mut h);
options.mask.hash(&mut h);
options.entries.hash(&mut h);
h.finish()
}
pub fn hash128_render(rfn: &RenderFn, args: &Option<ImageRenderArgs>, options: &ImageOptions) -> ImageHash {
use std::hash::Hash;
let mut h = ImageHash::hasher();
2u8.hash(&mut h);
(Arc::as_ptr(rfn) as usize).hash(&mut h);
args.hash(&mut h);
options.downscale.hash(&mut h);
options.mask.hash(&mut h);
options.entries.hash(&mut h);
h.finish()
}
}
impl ImageSource {
pub fn render<F, R>(new_img: F) -> Self
where
F: Fn(&ImageRenderArgs) -> R + Send + Sync + 'static,
R: ImageRenderWindowRoot,
{
let window = IMAGES_SV.read().render_windows();
Self::Render(
Arc::new(Box::new(move |args| {
if let Some(parent) = args.parent {
window.set_parent_in_window_context(parent);
}
let r = new_img(args);
window.enable_frame_capture_in_window_context(None);
Box::new(r)
})),
None,
)
}
pub fn render_node(render_mode: RenderMode, render: impl Fn(&ImageRenderArgs) -> UiNode + Send + Sync + 'static) -> Self {
let window = IMAGES_SV.read().render_windows();
Self::Render(
Arc::new(Box::new(move |args| {
if let Some(parent) = args.parent {
window.set_parent_in_window_context(parent);
}
let node = render(args);
window.enable_frame_capture_in_window_context(None);
window.new_window_root(node, render_mode)
})),
None,
)
}
}
impl ImageSource {
pub fn flood(size: impl Into<PxSize>, color: impl Into<Rgba>, density: Option<PxDensity2d>) -> Self {
Self::flood_impl(size.into(), color.into(), density)
}
fn flood_impl(size: PxSize, color: Rgba, density: Option<PxDensity2d>) -> Self {
let pixels = size.width.0 as usize * size.height.0 as usize;
let bgra = color.to_bgra_bytes();
let mut b = IpcBytes::new_mut_blocking(pixels * 4).expect("cannot allocate IpcBytes");
for b in b.chunks_exact_mut(4) {
b.copy_from_slice(&bgra);
}
Self::from_data(
b.finish_blocking().expect("cannot allocate IpcBytes"),
ImageDataFormat::Bgra8 {
size,
density,
original_color_type: ColorType::RGBA8,
},
)
}
pub fn linear_vertical(
size: impl Into<PxSize>,
stops: impl Into<GradientStops>,
density: Option<PxDensity2d>,
mask: Option<ImageMaskMode>,
) -> Self {
Self::linear_vertical_impl(size.into(), stops.into(), density, mask)
}
fn linear_vertical_impl(size: PxSize, stops: GradientStops, density: Option<PxDensity2d>, mask: Option<ImageMaskMode>) -> Self {
assert!(size.width > Px(0));
assert!(size.height > Px(0));
let mut line = PxLine::new(PxPoint::splat(Px(0)), PxPoint::new(Px(0), size.height));
let mut render_stops = vec![];
LAYOUT.with_root_context(LayoutPassId::new(), LayoutMetrics::new(1.fct(), size, Px(14)), || {
stops.layout_linear(LayoutAxis::Y, ExtendMode::Clamp, &mut line, &mut render_stops);
});
let line_a = line.start.y.0 as f32;
let line_b = line.end.y.0 as f32;
let mut bgra = Vec::with_capacity(size.height.0 as usize);
let mut render_stops = render_stops.into_iter();
let mut stop_a = render_stops.next().unwrap();
let mut stop_b = render_stops.next().unwrap();
'outer: for y in 0..size.height.0 {
let yf = y as f32;
let yf = (yf - line_a) / (line_b - line_a);
if yf < stop_a.offset {
bgra.push(stop_a.color.to_bgra_bytes());
continue;
}
while yf > stop_b.offset {
if let Some(next_b) = render_stops.next() {
stop_a = stop_b;
stop_b = next_b;
} else {
for _ in y..size.height.0 {
bgra.push(stop_b.color.to_bgra_bytes());
}
break 'outer;
}
}
let yf = (yf - stop_a.offset) / (stop_b.offset - stop_a.offset);
let sample = stop_a.color.lerp(&stop_b.color, yf.fct());
bgra.push(sample.to_bgra_bytes());
}
match mask {
Some(m) => {
let len = size.width.0 as usize * size.height.0 as usize;
let mut data = Vec::with_capacity(len);
for y in 0..size.height.0 {
let c = bgra[y as usize];
let c = match m {
ImageMaskMode::A => c[3],
ImageMaskMode::B => c[0],
ImageMaskMode::G => c[1],
ImageMaskMode::R => c[2],
ImageMaskMode::Luminance => {
let hsla = Hsla::from(Rgba::new(c[2], c[1], c[0], c[3]));
(hsla.lightness * 255.0).round().clamp(0.0, 255.0) as u8
}
_ => unreachable!(),
};
for _x in 0..size.width.0 {
data.push(c);
}
}
Self::from_data(
IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"),
ImageDataFormat::A8 { size },
)
}
None => {
let len = size.width.0 as usize * size.height.0 as usize * 4;
let mut data = Vec::with_capacity(len);
for y in 0..size.height.0 {
let c = bgra[y as usize];
for _x in 0..size.width.0 {
data.extend_from_slice(&c);
}
}
Self::from_data(
IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"),
ImageDataFormat::Bgra8 {
size,
density,
original_color_type: ColorType::RGBA8,
},
)
}
}
}
pub fn linear_horizontal(
size: impl Into<PxSize>,
stops: impl Into<GradientStops>,
density: Option<PxDensity2d>,
mask: Option<ImageMaskMode>,
) -> Self {
Self::linear_horizontal_impl(size.into(), stops.into(), density, mask)
}
fn linear_horizontal_impl(size: PxSize, stops: GradientStops, density: Option<PxDensity2d>, mask: Option<ImageMaskMode>) -> Self {
assert!(size.width > Px(0));
assert!(size.height > Px(0));
let mut line = PxLine::new(PxPoint::splat(Px(0)), PxPoint::new(size.width, Px(0)));
let mut render_stops = vec![];
LAYOUT.with_root_context(LayoutPassId::new(), LayoutMetrics::new(1.fct(), size, Px(14)), || {
stops.layout_linear(LayoutAxis::Y, ExtendMode::Clamp, &mut line, &mut render_stops);
});
let line_a = line.start.x.0 as f32;
let line_b = line.end.x.0 as f32;
let mut bgra = Vec::with_capacity(size.width.0 as usize);
let mut render_stops = render_stops.into_iter();
let mut stop_a = render_stops.next().unwrap();
let mut stop_b = render_stops.next().unwrap();
'outer: for x in 0..size.width.0 {
let xf = x as f32;
let xf = (xf - line_a) / (line_b - line_a);
if xf < stop_a.offset {
bgra.push(stop_a.color.to_bgra_bytes());
continue;
}
while xf > stop_b.offset {
if let Some(next_b) = render_stops.next() {
stop_a = stop_b;
stop_b = next_b;
} else {
for _ in x..size.width.0 {
bgra.push(stop_b.color.to_bgra_bytes());
}
break 'outer;
}
}
let xf = (xf - stop_a.offset) / (stop_b.offset - stop_a.offset);
let sample = stop_a.color.lerp(&stop_b.color, xf.fct());
bgra.push(sample.to_bgra_bytes());
}
match mask {
Some(m) => {
let len = size.width.0 as usize * size.height.0 as usize;
let mut data = Vec::with_capacity(len);
for _y in 0..size.height.0 {
for c in &bgra {
let c = match m {
ImageMaskMode::A => c[3],
ImageMaskMode::B => c[0],
ImageMaskMode::G => c[1],
ImageMaskMode::R => c[2],
ImageMaskMode::Luminance => {
let hsla = Hsla::from(Rgba::new(c[2], c[1], c[0], c[3]));
(hsla.lightness * 255.0).round().clamp(0.0, 255.0) as u8
}
_ => unreachable!(),
};
data.push(c);
}
}
Self::from_data(
IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"),
ImageDataFormat::A8 { size },
)
}
None => {
let len = size.width.0 as usize * size.height.0 as usize * 4;
let mut data = Vec::with_capacity(len);
for _y in 0..size.height.0 {
for c in &bgra {
data.extend_from_slice(c);
}
}
Self::from_data(
IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"),
ImageDataFormat::Bgra8 {
size,
density,
original_color_type: ColorType::RGBA8,
},
)
}
}
}
}
impl PartialEq for ImageSource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Read(l), Self::Read(r)) => l == r,
#[cfg(feature = "http")]
(Self::Download(lu, la), Self::Download(ru, ra)) => lu == ru && la == ra,
(Self::Render(lf, la), Self::Render(rf, ra)) => Arc::ptr_eq(lf, rf) && la == ra,
(Self::Image(l), Self::Image(r)) => l.var_eq(r),
(l, r) => {
let l_hash = match l {
ImageSource::Data(h, _, _) => h,
_ => return false,
};
let r_hash = match r {
ImageSource::Data(h, _, _) => h,
_ => return false,
};
l_hash == r_hash
}
}
}
}
impl fmt::Debug for ImageSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "ImageSource::")?;
}
match self {
ImageSource::Read(p) => f.debug_tuple("Read").field(p).finish(),
#[cfg(feature = "http")]
ImageSource::Download(u, a) => f.debug_tuple("Download").field(u).field(a).finish(),
ImageSource::Data(key, bytes, fmt) => f.debug_tuple("Data").field(key).field(bytes).field(fmt).finish(),
ImageSource::Render(_, _) => write!(f, "Render(_, _)"),
ImageSource::Image(_) => write!(f, "Image(_)"),
}
}
}
#[cfg(feature = "http")]
impl_from_and_into_var! {
fn from(uri: zng_task::http::Uri) -> ImageSource {
ImageSource::Download(uri, None)
}
fn from((uri, accept): (zng_task::http::Uri, &'static str)) -> ImageSource {
ImageSource::Download(uri, Some(accept.into()))
}
fn from(s: &str) -> ImageSource {
use zng_task::http::*;
if let Ok(uri) = Uri::try_from(s)
&& let Some(scheme) = uri.scheme()
{
if scheme == &uri::Scheme::HTTPS || scheme == &uri::Scheme::HTTP {
return ImageSource::Download(uri, None);
} else if scheme.as_str() == "file" {
return PathBuf::from(uri.path()).into();
}
}
PathBuf::from(s).into()
}
}
#[cfg(not(feature = "http"))]
impl_from_and_into_var! {
fn from(s: &str) -> ImageSource {
PathBuf::from(s).into()
}
}
impl_from_and_into_var! {
fn from(image: ImageVar) -> ImageSource {
ImageSource::Image(image)
}
fn from(path: PathBuf) -> ImageSource {
ImageSource::Read(path)
}
fn from(path: &Path) -> ImageSource {
path.to_owned().into()
}
fn from(s: String) -> ImageSource {
s.as_str().into()
}
fn from(s: Txt) -> ImageSource {
s.as_str().into()
}
fn from(data: &[u8]) -> ImageSource {
ImageSource::Data(
ImageHash::compute(data),
IpcBytes::from_slice_blocking(data).expect("cannot allocate IpcBytes"),
ImageDataFormat::Unknown,
)
}
fn from<const N: usize>(data: &[u8; N]) -> ImageSource {
(&data[..]).into()
}
fn from(data: IpcBytes) -> ImageSource {
ImageSource::Data(ImageHash::compute(&data[..]), data, ImageDataFormat::Unknown)
}
fn from(data: Vec<u8>) -> ImageSource {
IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes").into()
}
fn from<F: Into<ImageDataFormat>>((data, format): (&[u8], F)) -> ImageSource {
ImageSource::Data(
ImageHash::compute(data),
IpcBytes::from_slice_blocking(data).expect("cannot allocate IpcBytes"),
format.into(),
)
}
fn from<F: Into<ImageDataFormat>, const N: usize>((data, format): (&[u8; N], F)) -> ImageSource {
(&data[..], format).into()
}
fn from<F: Into<ImageDataFormat>>((data, format): (Vec<u8>, F)) -> ImageSource {
(IpcBytes::from_vec_blocking(data).expect("cannot allocate IpcBytes"), format).into()
}
fn from<F: Into<ImageDataFormat>>((data, format): (IpcBytes, F)) -> ImageSource {
ImageSource::Data(ImageHash::compute(&data[..]), data, format.into())
}
}
#[derive(Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ImageCacheMode {
Ignore,
Cache,
Retry,
Reload,
}
impl fmt::Debug for ImageCacheMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
write!(f, "CacheMode::")?;
}
match self {
Self::Ignore => write!(f, "Ignore"),
Self::Cache => write!(f, "Cache"),
Self::Retry => write!(f, "Retry"),
Self::Reload => write!(f, "Reload"),
}
}
}
impl_from_and_into_var! {
fn from(cache: bool) -> ImageCacheMode {
if cache { ImageCacheMode::Cache } else { ImageCacheMode::Ignore }
}
}
#[derive(Clone)]
pub enum ImageSourceFilter<U> {
BlockAll,
AllowAll,
Custom(Arc<dyn Fn(&U) -> bool + Send + Sync>),
}
impl<U> ImageSourceFilter<U> {
pub fn custom(allow: impl Fn(&U) -> bool + Send + Sync + 'static) -> Self {
Self::Custom(Arc::new(allow))
}
pub fn and(self, other: Self) -> Self
where
U: 'static,
{
use ImageSourceFilter::*;
match (self, other) {
(BlockAll, _) | (_, BlockAll) => BlockAll,
(AllowAll, _) | (_, AllowAll) => AllowAll,
(Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) && c1(u))),
}
}
pub fn or(self, other: Self) -> Self
where
U: 'static,
{
use ImageSourceFilter::*;
match (self, other) {
(AllowAll, _) | (_, AllowAll) => AllowAll,
(BlockAll, _) | (_, BlockAll) => BlockAll,
(Custom(c0), Custom(c1)) => Custom(Arc::new(move |u| c0(u) || c1(u))),
}
}
pub fn allows(&self, item: &U) -> bool {
match self {
ImageSourceFilter::BlockAll => false,
ImageSourceFilter::AllowAll => true,
ImageSourceFilter::Custom(f) => f(item),
}
}
}
impl<U> fmt::Debug for ImageSourceFilter<U> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BlockAll => write!(f, "BlockAll"),
Self::AllowAll => write!(f, "AllowAll"),
Self::Custom(_) => write!(f, "Custom(_)"),
}
}
}
impl<U: 'static> ops::BitAnd for ImageSourceFilter<U> {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
self.and(rhs)
}
}
impl<U: 'static> ops::BitOr for ImageSourceFilter<U> {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
self.or(rhs)
}
}
impl<U: 'static> ops::BitAndAssign for ImageSourceFilter<U> {
fn bitand_assign(&mut self, rhs: Self) {
*self = mem::replace(self, Self::BlockAll).and(rhs);
}
}
impl<U: 'static> ops::BitOrAssign for ImageSourceFilter<U> {
fn bitor_assign(&mut self, rhs: Self) {
*self = mem::replace(self, Self::BlockAll).or(rhs);
}
}
impl<U> PartialEq for ImageSourceFilter<U> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0),
_ => core::mem::discriminant(self) == core::mem::discriminant(other),
}
}
}
pub type PathFilter = ImageSourceFilter<PathBuf>;
impl PathFilter {
pub fn allow_dir(dir: impl AsRef<Path>) -> Self {
let dir = absolute_path(dir.as_ref(), || env::current_dir().expect("could not access current dir"), true);
PathFilter::custom(move |r| r.starts_with(&dir))
}
pub fn allow_ext(ext: impl Into<std::ffi::OsString>) -> Self {
let ext = ext.into();
PathFilter::custom(move |r| r.extension().map(|e| e == ext).unwrap_or(false))
}
pub fn allow_current_dir() -> Self {
PathFilter::custom(|r| env::current_dir().map(|d| r.starts_with(d)).unwrap_or(false))
}
pub fn allow_exe_dir() -> Self {
if let Ok(mut p) = env::current_exe().and_then(dunce::canonicalize)
&& p.pop()
{
return Self::allow_dir(p);
}
Self::custom(|_| false)
}
pub fn allow_res() -> Self {
Self::allow_dir(zng_env::res(""))
}
}
#[cfg(feature = "http")]
pub type UriFilter = ImageSourceFilter<zng_task::http::Uri>;
#[cfg(feature = "http")]
impl UriFilter {
pub fn allow_host(host: impl Into<Txt>) -> Self {
let host = host.into();
UriFilter::custom(move |u| u.authority().map(|a| a.host() == host).unwrap_or(false))
}
}
impl<F: Fn(&PathBuf) -> bool + Send + Sync + 'static> From<F> for PathFilter {
fn from(custom: F) -> Self {
PathFilter::custom(custom)
}
}
#[cfg(feature = "http")]
impl<F: Fn(&task::http::Uri) -> bool + Send + Sync + 'static> From<F> for UriFilter {
fn from(custom: F) -> Self {
UriFilter::custom(custom)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct ImageLimits {
pub max_encoded_len: ByteLength,
pub max_decoded_len: ByteLength,
pub allow_path: PathFilter,
#[cfg(feature = "http")]
pub allow_uri: UriFilter,
}
impl ImageLimits {
pub fn none() -> Self {
ImageLimits {
max_encoded_len: ByteLength::MAX,
max_decoded_len: ByteLength::MAX,
allow_path: PathFilter::AllowAll,
#[cfg(feature = "http")]
allow_uri: UriFilter::AllowAll,
}
}
pub fn with_max_encoded_len(mut self, max_encoded_len: impl Into<ByteLength>) -> Self {
self.max_encoded_len = max_encoded_len.into();
self
}
pub fn with_max_decoded_len(mut self, max_decoded_len: impl Into<ByteLength>) -> Self {
self.max_decoded_len = max_decoded_len.into();
self
}
pub fn with_allow_path(mut self, allow_path: impl Into<PathFilter>) -> Self {
self.allow_path = allow_path.into();
self
}
#[cfg(feature = "http")]
pub fn with_allow_uri(mut self, allow_url: impl Into<UriFilter>) -> Self {
self.allow_uri = allow_url.into();
self
}
}
impl Default for ImageLimits {
fn default() -> Self {
Self {
max_encoded_len: 100.megabytes(),
max_decoded_len: 4096.megabytes(),
allow_path: PathFilter::allow_res(),
#[cfg(feature = "http")]
allow_uri: UriFilter::BlockAll,
}
}
}
impl_from_and_into_var! {
fn from(some: ImageLimits) -> Option<ImageLimits>;
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ImageOptions {
pub cache_mode: ImageCacheMode,
pub downscale: Option<ImageDownscaleMode>,
pub mask: Option<ImageMaskMode>,
pub entries: ImageEntriesMode,
}
impl ImageOptions {
pub fn new(
cache_mode: ImageCacheMode,
downscale: Option<ImageDownscaleMode>,
mask: Option<ImageMaskMode>,
entries: ImageEntriesMode,
) -> Self {
Self {
cache_mode,
downscale,
mask,
entries,
}
}
pub fn cache() -> Self {
Self::new(ImageCacheMode::Cache, None, None, ImageEntriesMode::empty())
}
pub fn none() -> Self {
Self::new(ImageCacheMode::Ignore, None, None, ImageEntriesMode::empty())
}
}
fn absolute_path(path: &Path, base: impl FnOnce() -> PathBuf, allow_escape: bool) -> PathBuf {
if path.is_absolute() {
normalize_path(path)
} else {
let mut dir = base();
if allow_escape {
dir.push(path);
normalize_path(&dir)
} else {
dir.push(normalize_path(path));
dir
}
}
}
fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}