1use bitflags::bitflags;
4use serde::{Deserialize, Serialize};
5use zng_task::channel::IpcBytes;
6use zng_txt::Txt;
7
8use zng_unit::{Px, PxDensity2d, PxSize};
9
10use crate::api_extension::{ApiExtensionId, ApiExtensionPayload};
11
12crate::declare_id! {
13 pub struct ImageId(_);
17
18 pub struct ImageTextureId(_);
22
23 pub struct ImageEncodeId(_);
27}
28
29#[derive(Debug, Copy, Clone, Serialize, PartialEq, Eq, Hash, Deserialize, Default)]
31#[non_exhaustive]
32pub enum ImageMaskMode {
33 #[default]
37 A,
38 B,
42 G,
46 R,
50 Luminance,
54}
55
56bitflags! {
57 #[derive(Copy, Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
61 pub struct ImageEntriesMode: u8 {
62 const PAGES = 0b0001;
64 const REDUCED = 0b0010;
66 const PRIMARY = 0;
70
71 const OTHER = 0b1000;
73 }
74}
75#[cfg(feature = "var")]
76zng_var::impl_from_and_into_var! {
77 fn from(kind: ImageEntryKind) -> ImageEntriesMode {
78 match kind {
79 ImageEntryKind::Page => ImageEntriesMode::PAGES,
80 ImageEntryKind::Reduced { .. } => ImageEntriesMode::REDUCED,
81 ImageEntryKind::Other { .. } => ImageEntriesMode::OTHER,
82 }
83 }
84}
85
86#[derive(Debug, Clone)]
88#[cfg_attr(ipc, derive(Serialize, Deserialize))]
89#[non_exhaustive]
90pub struct ImageRequest<D> {
91 pub format: ImageDataFormat,
93 pub data: D,
100 pub max_decoded_len: u64,
105
106 pub downscale: Option<ImageDownscaleMode>,
113
114 pub mask: Option<ImageMaskMode>,
116
117 pub entries: ImageEntriesMode,
119
120 pub parent: Option<ImageEntryMetadata>,
125}
126impl<D> ImageRequest<D> {
127 pub fn new(
129 format: ImageDataFormat,
130 data: D,
131 max_decoded_len: u64,
132 downscale: Option<ImageDownscaleMode>,
133 mask: Option<ImageMaskMode>,
134 ) -> Self {
135 Self {
136 format,
137 data,
138 max_decoded_len,
139 downscale,
140 mask,
141 entries: ImageEntriesMode::PRIMARY,
142 parent: None,
143 }
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152#[non_exhaustive]
153pub enum ImageDownscaleMode {
154 Fit(PxSize),
156 Fill(PxSize),
158 MipMap {
162 min_size: PxSize,
164 max_size: PxSize,
166 },
167 Entries(Vec<ImageDownscaleMode>),
171}
172impl From<PxSize> for ImageDownscaleMode {
173 fn from(fit: PxSize) -> Self {
175 ImageDownscaleMode::Fit(fit)
176 }
177}
178impl From<Px> for ImageDownscaleMode {
179 fn from(fit: Px) -> Self {
181 ImageDownscaleMode::Fit(PxSize::splat(fit))
182 }
183}
184#[cfg(feature = "var")]
185zng_var::impl_from_and_into_var! {
186 fn from(fit: PxSize) -> ImageDownscaleMode;
187 fn from(fit: Px) -> ImageDownscaleMode;
188 fn from(some: ImageDownscaleMode) -> Option<ImageDownscaleMode>;
189}
190impl ImageDownscaleMode {
191 pub fn mip_map() -> Self {
193 Self::MipMap {
194 min_size: PxSize::splat(Px(512)),
195 max_size: PxSize::splat(Px::MAX),
196 }
197 }
198
199 pub fn with_entry(self, other: impl Into<ImageDownscaleMode>) -> Self {
201 self.with_impl(other.into())
202 }
203 fn with_impl(self, other: Self) -> Self {
204 let mut v = match self {
205 Self::Entries(e) => e,
206 s => vec![s],
207 };
208 match other {
209 Self::Entries(o) => v.extend(o),
210 o => v.push(o),
211 }
212 Self::Entries(v)
213 }
214
215 pub fn sizes(&self, page_size: PxSize, reduced_sizes: &[PxSize]) -> (Option<PxSize>, Vec<PxSize>) {
223 match self {
224 ImageDownscaleMode::Fit(s) => (downscale_fit_fill(page_size, *s, false), vec![]),
225 ImageDownscaleMode::Fill(s) => (downscale_fit_fill(page_size, *s, true), vec![]),
226 ImageDownscaleMode::MipMap { min_size, max_size } => Self::collect_mip_map(page_size, reduced_sizes, &[], *min_size, *max_size),
227 ImageDownscaleMode::Entries(modes) => {
228 let mut include_full_size = false;
229 let mut sizes = vec![];
230 let mut mip_map = None;
231 for m in modes {
232 m.collect_entries(page_size, &mut sizes, &mut mip_map, &mut include_full_size);
233 }
234 if let Some([min_size, max_size]) = mip_map {
235 let (first, mips) = Self::collect_mip_map(page_size, reduced_sizes, &sizes, min_size, max_size);
236 include_full_size |= first.is_some();
237 sizes.extend(first);
238 sizes.extend(mips);
239 }
240
241 sizes.sort_by_key(|s| s.width.0 * s.height.0);
242 sizes.dedup();
243
244 let full_downscale = if include_full_size { None } else { sizes.pop() };
245 sizes.reverse();
246
247 (full_downscale, sizes)
248 }
249 }
250 }
251
252 fn collect_mip_map(
253 page_size: PxSize,
254 reduced_sizes: &[PxSize],
255 entry_sizes: &[PxSize],
256 min_size: PxSize,
257 max_size: PxSize,
258 ) -> (Option<PxSize>, Vec<PxSize>) {
259 let page_downscale = downscale_fit_fill(page_size, max_size, true);
260 let mut size = page_downscale.unwrap_or(page_size) / Px(2);
261 let mut entries = vec![];
262 while min_size.width < size.width && min_size.height < size.height {
263 if let Some(entry) = downscale_fit_fill(page_size, size, true)
264 && !reduced_sizes.iter().any(|s| Self::near(entry, *s))
265 && !entry_sizes.iter().any(|s| Self::near(entry, *s))
266 {
267 entries.push(entry);
268 }
269 size /= Px(2);
270 }
271 (page_downscale, entries)
272 }
273 fn near(candidate: PxSize, existing: PxSize) -> bool {
274 let dist = (candidate - existing).abs();
275 dist.width < Px(10) && dist.height <= Px(10)
276 }
277
278 fn collect_entries(&self, page_size: PxSize, sizes: &mut Vec<PxSize>, mip_map: &mut Option<[PxSize; 2]>, include_full_size: &mut bool) {
279 match self {
280 ImageDownscaleMode::Fit(s) => match downscale_fit_fill(page_size, *s, false) {
281 Some(s) => sizes.push(s),
282 None => *include_full_size = true,
283 },
284 ImageDownscaleMode::Fill(s) => match downscale_fit_fill(page_size, *s, true) {
285 Some(s) => sizes.push(s),
286 None => *include_full_size = true,
287 },
288 ImageDownscaleMode::MipMap { min_size, max_size } => {
289 *include_full_size = true;
290 if let Some([min, max]) = mip_map {
291 *min = min.min(*min_size);
292 *max = max.min(*min_size);
293 } else {
294 *mip_map = Some([*min_size, *max_size]);
295 }
296 }
297 ImageDownscaleMode::Entries(modes) => {
298 for m in modes {
299 m.collect_entries(page_size, sizes, mip_map, include_full_size);
300 }
301 }
302 }
303 }
304}
305
306pub fn downscale_fit_fill(source_size: PxSize, constraints: PxSize, fill: bool) -> Option<PxSize> {
310 let source_size = source_size.cast::<f64>();
311 let new_size = constraints.cast::<f64>();
312
313 let w_ratio = new_size.width / source_size.width;
314 let h_ratio = new_size.height / source_size.height;
315
316 let ratio = if fill {
317 f64::max(w_ratio, h_ratio)
318 } else {
319 f64::min(w_ratio, h_ratio)
320 };
321
322 if ratio >= 1.0 {
323 return None;
324 }
325
326 let nw = u64::max((source_size.width * ratio).round() as _, 1);
327 let nh = u64::max((source_size.height * ratio).round() as _, 1);
328
329 const MAX: u64 = Px::MAX.0 as _;
330
331 let r = if nw > MAX {
332 let ratio = MAX as f64 / source_size.width;
333 (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
334 } else if nh > MAX {
335 let ratio = MAX as f64 / source_size.height;
336 (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
337 } else {
338 (Px(nw as _), Px(nh as _))
339 }
340 .into();
341
342 Some(r)
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347#[non_exhaustive]
348pub enum ImageDataFormat {
349 Bgra8 {
354 size: PxSize,
356 density: Option<PxDensity2d>,
358 original_color_type: ColorType,
360 },
361
362 A8 {
367 size: PxSize,
369 },
370
371 FileExtension(Txt),
376
377 MimeType(Txt),
382
383 Unknown,
387}
388impl From<Txt> for ImageDataFormat {
389 fn from(ext_or_mime: Txt) -> Self {
390 if ext_or_mime.contains('/') {
391 ImageDataFormat::MimeType(ext_or_mime)
392 } else {
393 ImageDataFormat::FileExtension(ext_or_mime)
394 }
395 }
396}
397impl From<&str> for ImageDataFormat {
398 fn from(ext_or_mime: &str) -> Self {
399 Txt::from_str(ext_or_mime).into()
400 }
401}
402impl From<PxSize> for ImageDataFormat {
403 fn from(bgra8_size: PxSize) -> Self {
404 ImageDataFormat::Bgra8 {
405 size: bgra8_size,
406 density: None,
407 original_color_type: ColorType::BGRA8,
408 }
409 }
410}
411impl PartialEq for ImageDataFormat {
412 fn eq(&self, other: &Self) -> bool {
413 match (self, other) {
414 (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
415 (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
416 (
417 Self::Bgra8 {
418 size: s0,
419 density: p0,
420 original_color_type: oc0,
421 },
422 Self::Bgra8 {
423 size: s1,
424 density: p1,
425 original_color_type: oc1,
426 },
427 ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
428 (Self::Unknown, Self::Unknown) => true,
429 _ => false,
430 }
431 }
432}
433impl Eq for ImageDataFormat {}
434impl std::hash::Hash for ImageDataFormat {
435 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
436 core::mem::discriminant(self).hash(state);
437 match self {
438 ImageDataFormat::Bgra8 {
439 size,
440 density,
441 original_color_type,
442 } => {
443 size.hash(state);
444 density_key(*density).hash(state);
445 original_color_type.hash(state)
446 }
447 ImageDataFormat::A8 { size } => {
448 size.hash(state);
449 }
450 ImageDataFormat::FileExtension(ext) => ext.hash(state),
451 ImageDataFormat::MimeType(mt) => mt.hash(state),
452 ImageDataFormat::Unknown => {}
453 }
454 }
455}
456
457fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
458 density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
459}
460
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463#[non_exhaustive]
464pub struct ImageEntryMetadata {
465 pub parent: ImageId,
469 pub index: usize,
471 pub kind: ImageEntryKind,
473}
474impl ImageEntryMetadata {
475 pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
477 Self { parent, index, kind }
478 }
479}
480
481#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
483#[non_exhaustive]
484pub struct ImageMetadata {
485 pub id: ImageId,
487 pub size: PxSize,
489 pub density: Option<PxDensity2d>,
491 pub is_mask: bool,
493 pub original_color_type: ColorType,
495 pub format_name: Txt,
497 pub parent: Option<ImageEntryMetadata>,
501
502 pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
504}
505impl ImageMetadata {
506 pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
508 Self {
509 id,
510 size,
511 density: None,
512 is_mask,
513 original_color_type,
514 parent: None,
515 extensions: vec![],
516 format_name: Txt::default(),
517 }
518 }
519}
520impl Default for ImageMetadata {
521 fn default() -> Self {
522 Self {
523 id: ImageId::INVALID,
524 size: Default::default(),
525 density: Default::default(),
526 is_mask: Default::default(),
527 original_color_type: ColorType::BGRA8,
528 parent: Default::default(),
529 extensions: vec![],
530 format_name: Txt::default(),
531 }
532 }
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
537#[non_exhaustive]
538pub enum ImageEntryKind {
539 Page,
541 Reduced {
545 synthetic: bool,
547 },
548 Other {
550 kind: Txt,
554 },
555}
556impl ImageEntryKind {
557 fn discriminant(&self) -> u8 {
558 match self {
559 ImageEntryKind::Page => 0,
560 ImageEntryKind::Reduced { .. } => 1,
561 ImageEntryKind::Other { .. } => 2,
562 }
563 }
564}
565impl std::cmp::Ord for ImageEntryKind {
566 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
567 self.discriminant().cmp(&other.discriminant())
568 }
569}
570impl std::cmp::PartialOrd for ImageEntryKind {
571 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
572 Some(self.cmp(other))
573 }
574}
575
576#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
582#[non_exhaustive]
583pub struct ImageDecoded {
584 pub meta: ImageMetadata,
586
587 pub partial: Option<PartialImageKind>,
593
594 pub pixels: IpcBytes,
598 pub is_opaque: bool,
600}
601impl Default for ImageDecoded {
602 fn default() -> Self {
603 Self {
604 meta: Default::default(),
605 partial: Default::default(),
606 pixels: Default::default(),
607 is_opaque: true,
608 }
609 }
610}
611impl ImageDecoded {
612 pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
614 Self {
615 meta,
616 partial: None,
617 pixels,
618 is_opaque,
619 }
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
625#[non_exhaustive]
626pub enum PartialImageKind {
627 Placeholder {
631 pixel_size: PxSize,
633 },
634 Rows {
638 y: Px,
642 height: Px,
644 },
645}
646
647bitflags! {
648 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
652 pub struct ImageFormatCapability: u32 {
653 const ENCODE = 1 << 0;
655 const DECODE_ENTRIES = 1 << 1;
657 const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
659 const DECODE_PROGRESSIVE = 1 << 3;
664 }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
671#[non_exhaustive]
672pub struct ImageFormat {
673 pub display_name: Txt,
675
676 pub media_type_suffixes: Txt,
680
681 pub file_extensions: Txt,
685
686 pub magic_numbers: Txt,
690
691 pub capabilities: ImageFormatCapability,
693}
694impl ImageFormat {
695 #[deprecated = "use `from_static2`, it will replace this function next breaking release"]
701 pub const fn from_static(
702 display_name: &'static str,
703 media_type_suffixes: &'static str,
704 file_extensions: &'static str,
705 capabilities: ImageFormatCapability,
706 ) -> Self {
707 assert!(media_type_suffixes.is_ascii());
708 Self {
709 display_name: Txt::from_static(display_name),
710 media_type_suffixes: Txt::from_static(media_type_suffixes),
711 file_extensions: Txt::from_static(file_extensions),
712 magic_numbers: Txt::from_static(""),
713 capabilities,
714 }
715 }
716
717 pub const fn from_static2(
723 display_name: &'static str,
724 media_type_suffixes: &'static str,
725 file_extensions: &'static str,
726 magic_numbers: &'static str,
727 capabilities: ImageFormatCapability,
728 ) -> Self {
729 assert!(media_type_suffixes.is_ascii());
730 assert!(magic_numbers.is_ascii());
731 Self {
732 display_name: Txt::from_static(display_name),
733 media_type_suffixes: Txt::from_static(media_type_suffixes),
734 file_extensions: Txt::from_static(file_extensions),
735 magic_numbers: Txt::from_static(magic_numbers),
736 capabilities,
737 }
738 }
739
740 pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
742 self.media_type_suffixes.split(',').map(|e| e.trim())
743 }
744
745 pub fn media_types(&self) -> impl Iterator<Item = Txt> {
747 self.media_type_suffixes_iter().map(Txt::from_str)
748 }
749
750 pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
752 self.file_extensions.split(',').map(|e| e.trim())
753 }
754
755 pub fn matches(&self, f: &str) -> bool {
759 let f = f.strip_prefix('.').unwrap_or(f);
760 let f = f.strip_prefix("image/").unwrap_or(f);
761 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))
762 }
763
764 pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
768 'search: for magic in self.magic_numbers.split(',') {
769 if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
770 continue 'search;
771 }
772 'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
773 if c == b"xx" {
774 continue 'm;
775 }
776 fn decode(c: u8) -> u8 {
777 if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
778 }
779 let c = (decode(c[0]) << 4) | decode(c[1]);
780 if c != *b {
781 continue 'search;
782 }
783 }
784 return true;
785 }
786 false
787 }
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
792#[non_exhaustive]
793pub struct ColorType {
794 pub name: Txt,
796 pub bits: u8,
798 pub channels: u8,
800}
801impl ColorType {
802 pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
804 Self { name, bits, channels }
805 }
806
807 pub fn bits_per_pixel(&self) -> u16 {
809 self.bits as u16 * self.channels as u16
810 }
811
812 pub fn bytes_per_pixel(&self) -> u16 {
814 self.bits_per_pixel() / 8
815 }
816}
817impl ColorType {
818 pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
820 pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
822
823 pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
829#[non_exhaustive]
830pub struct ImageEncodeRequest {
831 pub id: ImageId,
833
834 pub entries: Vec<(ImageId, ImageEntryKind)>,
838
839 pub format: Txt,
841}
842impl ImageEncodeRequest {
843 pub fn new(id: ImageId, format: Txt) -> Self {
845 Self {
846 id,
847 entries: vec![],
848 format,
849 }
850 }
851}