1use bitflags::bitflags;
4use serde::{Deserialize, Serialize};
5use zng_task::channel::IpcBytes;
6use zng_txt::Txt;
7
8use zng_unit::{Px, PxDensity2d, PxSize};
9
10crate::declare_id! {
11 pub struct ImageId(_);
15
16 pub struct ImageTextureId(_);
20
21 pub struct ImageEncodeId(_);
25}
26
27#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Hash, Deserialize, Default)]
29#[non_exhaustive]
30pub enum ImageMaskMode {
31 #[default]
35 A,
36 B,
40 G,
44 R,
48 Luminance,
52}
53
54bitflags! {
55 #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
59 pub struct ImageEntriesMode: u8 {
60 const PAGES = 0b0001;
62 const REDUCED = 0b0010;
64 const PRIMARY = 0;
68
69 const OTHER = 0b1000;
71 }
72}
73#[cfg(feature = "var")]
74zng_var::impl_from_and_into_var! {
75 fn from(kind: ImageEntryKind) -> ImageEntriesMode {
76 match kind {
77 ImageEntryKind::Page => ImageEntriesMode::PAGES,
78 ImageEntryKind::Reduced { .. } => ImageEntriesMode::REDUCED,
79 ImageEntryKind::Other { .. } => ImageEntriesMode::OTHER,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86#[non_exhaustive]
87pub struct ImageRequest<D> {
88 pub format: ImageDataFormat,
90 pub data: D,
96 pub max_decoded_len: u64,
101
102 pub downscale: Option<ImageDownscaleMode>,
109
110 pub mask: Option<ImageMaskMode>,
112
113 pub entries: ImageEntriesMode,
115
116 pub parent: Option<ImageEntryMetadata>,
121}
122impl<D> ImageRequest<D> {
123 pub fn new(
125 format: ImageDataFormat,
126 data: D,
127 max_decoded_len: u64,
128 downscale: Option<ImageDownscaleMode>,
129 mask: Option<ImageMaskMode>,
130 ) -> Self {
131 Self {
132 format,
133 data,
134 max_decoded_len,
135 downscale,
136 mask,
137 entries: ImageEntriesMode::PRIMARY,
138 parent: None,
139 }
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[non_exhaustive]
149pub enum ImageDownscaleMode {
150 Fit(PxSize),
152 Fill(PxSize),
154 MipMap {
158 min_size: PxSize,
160 max_size: PxSize,
162 },
163 Entries(Vec<ImageDownscaleMode>),
167}
168impl From<PxSize> for ImageDownscaleMode {
169 fn from(fit: PxSize) -> Self {
171 ImageDownscaleMode::Fit(fit)
172 }
173}
174impl From<Px> for ImageDownscaleMode {
175 fn from(fit: Px) -> Self {
177 ImageDownscaleMode::Fit(PxSize::splat(fit))
178 }
179}
180#[cfg(feature = "var")]
181zng_var::impl_from_and_into_var! {
182 fn from(fit: PxSize) -> ImageDownscaleMode;
183 fn from(fit: Px) -> ImageDownscaleMode;
184 fn from(some: ImageDownscaleMode) -> Option<ImageDownscaleMode>;
185}
186impl ImageDownscaleMode {
187 pub fn mip_map() -> Self {
189 Self::MipMap {
190 min_size: PxSize::splat(Px(512)),
191 max_size: PxSize::splat(Px::MAX),
192 }
193 }
194
195 pub fn with_entry(self, other: impl Into<ImageDownscaleMode>) -> Self {
197 self.with_impl(other.into())
198 }
199 fn with_impl(self, other: Self) -> Self {
200 let mut v = match self {
201 Self::Entries(e) => e,
202 s => vec![s],
203 };
204 match other {
205 Self::Entries(o) => v.extend(o),
206 o => v.push(o),
207 }
208 Self::Entries(v)
209 }
210
211 pub fn sizes(&self, page_size: PxSize, reduced_sizes: &[PxSize]) -> (Option<PxSize>, Vec<PxSize>) {
219 match self {
220 ImageDownscaleMode::Fit(s) => (fit_fill(page_size, *s, false), vec![]),
221 ImageDownscaleMode::Fill(s) => (fit_fill(page_size, *s, true), vec![]),
222 ImageDownscaleMode::MipMap { min_size, max_size } => Self::collect_mip_map(page_size, reduced_sizes, &[], *min_size, *max_size),
223 ImageDownscaleMode::Entries(modes) => {
224 let mut include_full_size = false;
225 let mut sizes = vec![];
226 let mut mip_map = None;
227 for m in modes {
228 m.collect_entries(page_size, &mut sizes, &mut mip_map, &mut include_full_size);
229 }
230 if let Some([min_size, max_size]) = mip_map {
231 let (first, mips) = Self::collect_mip_map(page_size, reduced_sizes, &sizes, min_size, max_size);
232 include_full_size |= first.is_some();
233 sizes.extend(first);
234 sizes.extend(mips);
235 }
236
237 sizes.sort_by_key(|s| s.width.0 * s.height.0);
238 sizes.dedup();
239
240 let full_downscale = if include_full_size { None } else { sizes.pop() };
241 sizes.reverse();
242
243 (full_downscale, sizes)
244 }
245 }
246 }
247
248 fn collect_mip_map(
249 page_size: PxSize,
250 reduced_sizes: &[PxSize],
251 entry_sizes: &[PxSize],
252 min_size: PxSize,
253 max_size: PxSize,
254 ) -> (Option<PxSize>, Vec<PxSize>) {
255 let page_downscale = fit_fill(page_size, max_size, true);
256 let mut size = page_downscale.unwrap_or(page_size) / Px(2);
257 let mut entries = vec![];
258 while min_size.width < size.width && min_size.height < size.height {
259 if let Some(entry) = fit_fill(page_size, size, true)
260 && !reduced_sizes.iter().any(|s| Self::near(entry, *s))
261 && !entry_sizes.iter().any(|s| Self::near(entry, *s))
262 {
263 entries.push(entry);
264 }
265 size /= Px(2);
266 }
267 (page_downscale, entries)
268 }
269 fn near(candidate: PxSize, existing: PxSize) -> bool {
270 let dist = (candidate - existing).abs();
271 dist.width < Px(10) && dist.height <= Px(10)
272 }
273
274 fn collect_entries(&self, page_size: PxSize, sizes: &mut Vec<PxSize>, mip_map: &mut Option<[PxSize; 2]>, include_full_size: &mut bool) {
275 match self {
276 ImageDownscaleMode::Fit(s) => match fit_fill(page_size, *s, false) {
277 Some(s) => sizes.push(s),
278 None => *include_full_size = true,
279 },
280 ImageDownscaleMode::Fill(s) => match fit_fill(page_size, *s, true) {
281 Some(s) => sizes.push(s),
282 None => *include_full_size = true,
283 },
284 ImageDownscaleMode::MipMap { min_size, max_size } => {
285 *include_full_size = true;
286 if let Some([min, max]) = mip_map {
287 *min = min.min(*min_size);
288 *max = max.min(*min_size);
289 } else {
290 *mip_map = Some([*min_size, *max_size]);
291 }
292 }
293 ImageDownscaleMode::Entries(modes) => {
294 for m in modes {
295 m.collect_entries(page_size, sizes, mip_map, include_full_size);
296 }
297 }
298 }
299 }
300}
301
302fn fit_fill(source_size: PxSize, new_size: PxSize, fill: bool) -> Option<PxSize> {
303 let source_size = source_size.cast::<f64>();
304 let new_size = new_size.cast::<f64>();
305
306 let w_ratio = new_size.width / source_size.width;
307 let h_ratio = new_size.height / source_size.height;
308
309 let ratio = if fill {
310 f64::max(w_ratio, h_ratio)
311 } else {
312 f64::min(w_ratio, h_ratio)
313 };
314
315 if ratio >= 1.0 {
316 return None;
317 }
318
319 let nw = u64::max((source_size.width * ratio).round() as _, 1);
320 let nh = u64::max((source_size.height * ratio).round() as _, 1);
321
322 const MAX: u64 = Px::MAX.0 as _;
323
324 let r = if nw > MAX {
325 let ratio = MAX as f64 / source_size.width;
326 (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
327 } else if nh > MAX {
328 let ratio = MAX as f64 / source_size.height;
329 (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
330 } else {
331 (Px(nw as _), Px(nh as _))
332 }
333 .into();
334
335 Some(r)
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340#[non_exhaustive]
341pub enum ImageDataFormat {
342 Bgra8 {
347 size: PxSize,
349 density: Option<PxDensity2d>,
351 original_color_type: ColorType,
353 },
354
355 A8 {
360 size: PxSize,
362 },
363
364 FileExtension(Txt),
369
370 MimeType(Txt),
375
376 Unknown,
380}
381impl From<Txt> for ImageDataFormat {
382 fn from(ext_or_mime: Txt) -> Self {
383 if ext_or_mime.contains('/') {
384 ImageDataFormat::MimeType(ext_or_mime)
385 } else {
386 ImageDataFormat::FileExtension(ext_or_mime)
387 }
388 }
389}
390impl From<&str> for ImageDataFormat {
391 fn from(ext_or_mime: &str) -> Self {
392 Txt::from_str(ext_or_mime).into()
393 }
394}
395impl From<PxSize> for ImageDataFormat {
396 fn from(bgra8_size: PxSize) -> Self {
397 ImageDataFormat::Bgra8 {
398 size: bgra8_size,
399 density: None,
400 original_color_type: ColorType::BGRA8,
401 }
402 }
403}
404impl PartialEq for ImageDataFormat {
405 fn eq(&self, other: &Self) -> bool {
406 match (self, other) {
407 (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
408 (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
409 (
410 Self::Bgra8 {
411 size: s0,
412 density: p0,
413 original_color_type: oc0,
414 },
415 Self::Bgra8 {
416 size: s1,
417 density: p1,
418 original_color_type: oc1,
419 },
420 ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
421 (Self::Unknown, Self::Unknown) => true,
422 _ => false,
423 }
424 }
425}
426impl Eq for ImageDataFormat {}
427impl std::hash::Hash for ImageDataFormat {
428 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
429 core::mem::discriminant(self).hash(state);
430 match self {
431 ImageDataFormat::Bgra8 {
432 size,
433 density,
434 original_color_type,
435 } => {
436 size.hash(state);
437 density_key(*density).hash(state);
438 original_color_type.hash(state)
439 }
440 ImageDataFormat::A8 { size } => {
441 size.hash(state);
442 }
443 ImageDataFormat::FileExtension(ext) => ext.hash(state),
444 ImageDataFormat::MimeType(mt) => mt.hash(state),
445 ImageDataFormat::Unknown => {}
446 }
447 }
448}
449
450fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
451 density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
452}
453
454#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
456#[non_exhaustive]
457pub struct ImageEntryMetadata {
458 pub parent: ImageId,
462 pub index: usize,
464 pub kind: ImageEntryKind,
466}
467impl ImageEntryMetadata {
468 pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
470 Self { parent, index, kind }
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
476#[non_exhaustive]
477pub struct ImageMetadata {
478 pub id: ImageId,
480 pub size: PxSize,
482 pub density: Option<PxDensity2d>,
484 pub is_mask: bool,
486 pub original_color_type: ColorType,
488 pub parent: Option<ImageEntryMetadata>,
492}
493impl ImageMetadata {
494 pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
496 Self {
497 id,
498 size,
499 density: None,
500 is_mask,
501 original_color_type,
502 parent: None,
503 }
504 }
505}
506impl Default for ImageMetadata {
507 fn default() -> Self {
508 Self {
509 id: ImageId::INVALID,
510 size: Default::default(),
511 density: Default::default(),
512 is_mask: Default::default(),
513 original_color_type: ColorType::BGRA8,
514 parent: Default::default(),
515 }
516 }
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
521#[non_exhaustive]
522pub enum ImageEntryKind {
523 Page,
525 Reduced {
529 synthetic: bool,
531 },
532 Other {
534 kind: Txt,
538 },
539}
540impl ImageEntryKind {
541 fn discriminant(&self) -> u8 {
542 match self {
543 ImageEntryKind::Page => 0,
544 ImageEntryKind::Reduced { .. } => 1,
545 ImageEntryKind::Other { .. } => 2,
546 }
547 }
548}
549impl std::cmp::Ord for ImageEntryKind {
550 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
551 self.discriminant().cmp(&other.discriminant())
552 }
553}
554impl std::cmp::PartialOrd for ImageEntryKind {
555 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
556 Some(self.cmp(other))
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
566#[non_exhaustive]
567pub struct ImageDecoded {
568 pub meta: ImageMetadata,
570
571 pub partial: Option<PartialImageKind>,
577
578 pub pixels: IpcBytes,
582 pub is_opaque: bool,
584}
585impl Default for ImageDecoded {
586 fn default() -> Self {
587 Self {
588 meta: Default::default(),
589 partial: Default::default(),
590 pixels: Default::default(),
591 is_opaque: true,
592 }
593 }
594}
595impl ImageDecoded {
596 pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
598 Self {
599 meta,
600 partial: None,
601 pixels,
602 is_opaque,
603 }
604 }
605}
606
607#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
609#[non_exhaustive]
610pub enum PartialImageKind {
611 Placeholder {
615 pixel_size: PxSize,
617 },
618 Rows {
622 y: Px,
626 height: Px,
628 },
629}
630
631bitflags! {
632 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
636 pub struct ImageFormatCapability: u32 {
637 const ENCODE = 1 << 0;
639 const DECODE_ENTRIES = 1 << 1;
641 const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
643 const DECODE_PROGRESSIVE = 1 << 3;
648 }
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
655#[non_exhaustive]
656pub struct ImageFormat {
657 pub display_name: Txt,
659
660 pub media_type_suffixes: Txt,
664
665 pub file_extensions: Txt,
669
670 pub capabilities: ImageFormatCapability,
672}
673impl ImageFormat {
674 pub const fn from_static(
680 display_name: &'static str,
681 media_type_suffixes: &'static str,
682 file_extensions: &'static str,
683 capabilities: ImageFormatCapability,
684 ) -> Self {
685 assert!(media_type_suffixes.is_ascii());
686 Self {
687 display_name: Txt::from_static(display_name),
688 media_type_suffixes: Txt::from_static(media_type_suffixes),
689 file_extensions: Txt::from_static(file_extensions),
690 capabilities,
691 }
692 }
693
694 pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
696 self.media_type_suffixes.split(',').map(|e| e.trim())
697 }
698
699 pub fn media_types(&self) -> impl Iterator<Item = Txt> {
701 self.media_type_suffixes_iter().map(Txt::from_str)
702 }
703
704 pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
706 self.file_extensions.split(',').map(|e| e.trim())
707 }
708
709 pub fn matches(&self, f: &str) -> bool {
713 let f = f.strip_prefix('.').unwrap_or(f);
714 let f = f.strip_prefix("image/").unwrap_or(f);
715 self.media_type_suffixes_iter().any(|e| e.eq_ignore_ascii_case(f)) || self.file_extensions_iter().any(|e| e.eq_ignore_ascii_case(f))
716 }
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
721#[non_exhaustive]
722pub struct ColorType {
723 pub name: Txt,
725 pub bits: u8,
727 pub channels: u8,
729}
730impl ColorType {
731 pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
733 Self { name, bits, channels }
734 }
735
736 pub fn bits_per_pixel(&self) -> u16 {
738 self.bits as u16 * self.channels as u16
739 }
740
741 pub fn bytes_per_pixel(&self) -> u16 {
743 self.bits_per_pixel() / 8
744 }
745}
746impl ColorType {
747 pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
749 pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
751
752 pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
758#[non_exhaustive]
759pub struct ImageEncodeRequest {
760 pub id: ImageId,
762
763 pub entries: Vec<(ImageId, ImageEntryKind)>,
767
768 pub format: Txt,
770}
771impl ImageEncodeRequest {
772 pub fn new(id: ImageId, format: Txt) -> Self {
774 Self {
775 id,
776 entries: vec![],
777 format,
778 }
779 }
780}