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) => (fit_fill(page_size, *s, false), vec![]),
225 ImageDownscaleMode::Fill(s) => (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 = 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) = 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 fit_fill(page_size, *s, false) {
281 Some(s) => sizes.push(s),
282 None => *include_full_size = true,
283 },
284 ImageDownscaleMode::Fill(s) => match 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
306fn fit_fill(source_size: PxSize, new_size: PxSize, fill: bool) -> Option<PxSize> {
307 let source_size = source_size.cast::<f64>();
308 let new_size = new_size.cast::<f64>();
309
310 let w_ratio = new_size.width / source_size.width;
311 let h_ratio = new_size.height / source_size.height;
312
313 let ratio = if fill {
314 f64::max(w_ratio, h_ratio)
315 } else {
316 f64::min(w_ratio, h_ratio)
317 };
318
319 if ratio >= 1.0 {
320 return None;
321 }
322
323 let nw = u64::max((source_size.width * ratio).round() as _, 1);
324 let nh = u64::max((source_size.height * ratio).round() as _, 1);
325
326 const MAX: u64 = Px::MAX.0 as _;
327
328 let r = if nw > MAX {
329 let ratio = MAX as f64 / source_size.width;
330 (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
331 } else if nh > MAX {
332 let ratio = MAX as f64 / source_size.height;
333 (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
334 } else {
335 (Px(nw as _), Px(nh as _))
336 }
337 .into();
338
339 Some(r)
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344#[non_exhaustive]
345pub enum ImageDataFormat {
346 Bgra8 {
351 size: PxSize,
353 density: Option<PxDensity2d>,
355 original_color_type: ColorType,
357 },
358
359 A8 {
364 size: PxSize,
366 },
367
368 FileExtension(Txt),
373
374 MimeType(Txt),
379
380 Unknown,
384}
385impl From<Txt> for ImageDataFormat {
386 fn from(ext_or_mime: Txt) -> Self {
387 if ext_or_mime.contains('/') {
388 ImageDataFormat::MimeType(ext_or_mime)
389 } else {
390 ImageDataFormat::FileExtension(ext_or_mime)
391 }
392 }
393}
394impl From<&str> for ImageDataFormat {
395 fn from(ext_or_mime: &str) -> Self {
396 Txt::from_str(ext_or_mime).into()
397 }
398}
399impl From<PxSize> for ImageDataFormat {
400 fn from(bgra8_size: PxSize) -> Self {
401 ImageDataFormat::Bgra8 {
402 size: bgra8_size,
403 density: None,
404 original_color_type: ColorType::BGRA8,
405 }
406 }
407}
408impl PartialEq for ImageDataFormat {
409 fn eq(&self, other: &Self) -> bool {
410 match (self, other) {
411 (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
412 (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
413 (
414 Self::Bgra8 {
415 size: s0,
416 density: p0,
417 original_color_type: oc0,
418 },
419 Self::Bgra8 {
420 size: s1,
421 density: p1,
422 original_color_type: oc1,
423 },
424 ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
425 (Self::Unknown, Self::Unknown) => true,
426 _ => false,
427 }
428 }
429}
430impl Eq for ImageDataFormat {}
431impl std::hash::Hash for ImageDataFormat {
432 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
433 core::mem::discriminant(self).hash(state);
434 match self {
435 ImageDataFormat::Bgra8 {
436 size,
437 density,
438 original_color_type,
439 } => {
440 size.hash(state);
441 density_key(*density).hash(state);
442 original_color_type.hash(state)
443 }
444 ImageDataFormat::A8 { size } => {
445 size.hash(state);
446 }
447 ImageDataFormat::FileExtension(ext) => ext.hash(state),
448 ImageDataFormat::MimeType(mt) => mt.hash(state),
449 ImageDataFormat::Unknown => {}
450 }
451 }
452}
453
454fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
455 density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
456}
457
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
460#[non_exhaustive]
461pub struct ImageEntryMetadata {
462 pub parent: ImageId,
466 pub index: usize,
468 pub kind: ImageEntryKind,
470}
471impl ImageEntryMetadata {
472 pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
474 Self { parent, index, kind }
475 }
476}
477
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
480#[non_exhaustive]
481pub struct ImageMetadata {
482 pub id: ImageId,
484 pub size: PxSize,
486 pub density: Option<PxDensity2d>,
488 pub is_mask: bool,
490 pub original_color_type: ColorType,
492 pub format_name: Txt,
494 pub parent: Option<ImageEntryMetadata>,
498
499 pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
501}
502impl ImageMetadata {
503 pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
505 Self {
506 id,
507 size,
508 density: None,
509 is_mask,
510 original_color_type,
511 parent: None,
512 extensions: vec![],
513 format_name: Txt::default(),
514 }
515 }
516}
517impl Default for ImageMetadata {
518 fn default() -> Self {
519 Self {
520 id: ImageId::INVALID,
521 size: Default::default(),
522 density: Default::default(),
523 is_mask: Default::default(),
524 original_color_type: ColorType::BGRA8,
525 parent: Default::default(),
526 extensions: vec![],
527 format_name: Txt::default(),
528 }
529 }
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
534#[non_exhaustive]
535pub enum ImageEntryKind {
536 Page,
538 Reduced {
542 synthetic: bool,
544 },
545 Other {
547 kind: Txt,
551 },
552}
553impl ImageEntryKind {
554 fn discriminant(&self) -> u8 {
555 match self {
556 ImageEntryKind::Page => 0,
557 ImageEntryKind::Reduced { .. } => 1,
558 ImageEntryKind::Other { .. } => 2,
559 }
560 }
561}
562impl std::cmp::Ord for ImageEntryKind {
563 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
564 self.discriminant().cmp(&other.discriminant())
565 }
566}
567impl std::cmp::PartialOrd for ImageEntryKind {
568 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
569 Some(self.cmp(other))
570 }
571}
572
573#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
579#[non_exhaustive]
580pub struct ImageDecoded {
581 pub meta: ImageMetadata,
583
584 pub partial: Option<PartialImageKind>,
590
591 pub pixels: IpcBytes,
595 pub is_opaque: bool,
597}
598impl Default for ImageDecoded {
599 fn default() -> Self {
600 Self {
601 meta: Default::default(),
602 partial: Default::default(),
603 pixels: Default::default(),
604 is_opaque: true,
605 }
606 }
607}
608impl ImageDecoded {
609 pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
611 Self {
612 meta,
613 partial: None,
614 pixels,
615 is_opaque,
616 }
617 }
618}
619
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
622#[non_exhaustive]
623pub enum PartialImageKind {
624 Placeholder {
628 pixel_size: PxSize,
630 },
631 Rows {
635 y: Px,
639 height: Px,
641 },
642}
643
644bitflags! {
645 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
649 pub struct ImageFormatCapability: u32 {
650 const ENCODE = 1 << 0;
652 const DECODE_ENTRIES = 1 << 1;
654 const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
656 const DECODE_PROGRESSIVE = 1 << 3;
661 }
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
668#[non_exhaustive]
669pub struct ImageFormat {
670 pub display_name: Txt,
672
673 pub media_type_suffixes: Txt,
677
678 pub file_extensions: Txt,
682
683 pub magic_numbers: Txt,
687
688 pub capabilities: ImageFormatCapability,
690}
691impl ImageFormat {
692 #[deprecated = "use `from_static2`, it will replace this function next breaking release"]
698 pub const fn from_static(
699 display_name: &'static str,
700 media_type_suffixes: &'static str,
701 file_extensions: &'static str,
702 capabilities: ImageFormatCapability,
703 ) -> Self {
704 assert!(media_type_suffixes.is_ascii());
705 Self {
706 display_name: Txt::from_static(display_name),
707 media_type_suffixes: Txt::from_static(media_type_suffixes),
708 file_extensions: Txt::from_static(file_extensions),
709 magic_numbers: Txt::from_static(""),
710 capabilities,
711 }
712 }
713
714 pub const fn from_static2(
720 display_name: &'static str,
721 media_type_suffixes: &'static str,
722 file_extensions: &'static str,
723 magic_numbers: &'static str,
724 capabilities: ImageFormatCapability,
725 ) -> Self {
726 assert!(media_type_suffixes.is_ascii());
727 assert!(magic_numbers.is_ascii());
728 Self {
729 display_name: Txt::from_static(display_name),
730 media_type_suffixes: Txt::from_static(media_type_suffixes),
731 file_extensions: Txt::from_static(file_extensions),
732 magic_numbers: Txt::from_static(magic_numbers),
733 capabilities,
734 }
735 }
736
737 pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
739 self.media_type_suffixes.split(',').map(|e| e.trim())
740 }
741
742 pub fn media_types(&self) -> impl Iterator<Item = Txt> {
744 self.media_type_suffixes_iter().map(Txt::from_str)
745 }
746
747 pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
749 self.file_extensions.split(',').map(|e| e.trim())
750 }
751
752 pub fn matches(&self, f: &str) -> bool {
756 let f = f.strip_prefix('.').unwrap_or(f);
757 let f = f.strip_prefix("image/").unwrap_or(f);
758 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))
759 }
760
761 pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
765 'search: for magic in self.magic_numbers.split(',') {
766 if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
767 continue 'search;
768 }
769 'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
770 if c == b"xx" {
771 continue 'm;
772 }
773 fn decode(c: u8) -> u8 {
774 if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
775 }
776 let c = (decode(c[0]) << 4) | decode(c[1]);
777 if c != *b {
778 continue 'search;
779 }
780 }
781 return true;
782 }
783 false
784 }
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
789#[non_exhaustive]
790pub struct ColorType {
791 pub name: Txt,
793 pub bits: u8,
795 pub channels: u8,
797}
798impl ColorType {
799 pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
801 Self { name, bits, channels }
802 }
803
804 pub fn bits_per_pixel(&self) -> u16 {
806 self.bits as u16 * self.channels as u16
807 }
808
809 pub fn bytes_per_pixel(&self) -> u16 {
811 self.bits_per_pixel() / 8
812 }
813}
814impl ColorType {
815 pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
817 pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
819
820 pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
826#[non_exhaustive]
827pub struct ImageEncodeRequest {
828 pub id: ImageId,
830
831 pub entries: Vec<(ImageId, ImageEntryKind)>,
835
836 pub format: Txt,
838}
839impl ImageEncodeRequest {
840 pub fn new(id: ImageId, format: Txt) -> Self {
842 Self {
843 id,
844 entries: vec![],
845 format,
846 }
847 }
848}