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, Serialize, Deserialize)]
88#[non_exhaustive]
89pub struct ImageRequest<D> {
90 pub format: ImageDataFormat,
92 pub data: D,
98 pub max_decoded_len: u64,
103
104 pub downscale: Option<ImageDownscaleMode>,
111
112 pub mask: Option<ImageMaskMode>,
114
115 pub entries: ImageEntriesMode,
117
118 pub parent: Option<ImageEntryMetadata>,
123}
124impl<D> ImageRequest<D> {
125 pub fn new(
127 format: ImageDataFormat,
128 data: D,
129 max_decoded_len: u64,
130 downscale: Option<ImageDownscaleMode>,
131 mask: Option<ImageMaskMode>,
132 ) -> Self {
133 Self {
134 format,
135 data,
136 max_decoded_len,
137 downscale,
138 mask,
139 entries: ImageEntriesMode::PRIMARY,
140 parent: None,
141 }
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
150#[non_exhaustive]
151pub enum ImageDownscaleMode {
152 Fit(PxSize),
154 Fill(PxSize),
156 MipMap {
160 min_size: PxSize,
162 max_size: PxSize,
164 },
165 Entries(Vec<ImageDownscaleMode>),
169}
170impl From<PxSize> for ImageDownscaleMode {
171 fn from(fit: PxSize) -> Self {
173 ImageDownscaleMode::Fit(fit)
174 }
175}
176impl From<Px> for ImageDownscaleMode {
177 fn from(fit: Px) -> Self {
179 ImageDownscaleMode::Fit(PxSize::splat(fit))
180 }
181}
182#[cfg(feature = "var")]
183zng_var::impl_from_and_into_var! {
184 fn from(fit: PxSize) -> ImageDownscaleMode;
185 fn from(fit: Px) -> ImageDownscaleMode;
186 fn from(some: ImageDownscaleMode) -> Option<ImageDownscaleMode>;
187}
188impl ImageDownscaleMode {
189 pub fn mip_map() -> Self {
191 Self::MipMap {
192 min_size: PxSize::splat(Px(512)),
193 max_size: PxSize::splat(Px::MAX),
194 }
195 }
196
197 pub fn with_entry(self, other: impl Into<ImageDownscaleMode>) -> Self {
199 self.with_impl(other.into())
200 }
201 fn with_impl(self, other: Self) -> Self {
202 let mut v = match self {
203 Self::Entries(e) => e,
204 s => vec![s],
205 };
206 match other {
207 Self::Entries(o) => v.extend(o),
208 o => v.push(o),
209 }
210 Self::Entries(v)
211 }
212
213 pub fn sizes(&self, page_size: PxSize, reduced_sizes: &[PxSize]) -> (Option<PxSize>, Vec<PxSize>) {
221 match self {
222 ImageDownscaleMode::Fit(s) => (fit_fill(page_size, *s, false), vec![]),
223 ImageDownscaleMode::Fill(s) => (fit_fill(page_size, *s, true), vec![]),
224 ImageDownscaleMode::MipMap { min_size, max_size } => Self::collect_mip_map(page_size, reduced_sizes, &[], *min_size, *max_size),
225 ImageDownscaleMode::Entries(modes) => {
226 let mut include_full_size = false;
227 let mut sizes = vec![];
228 let mut mip_map = None;
229 for m in modes {
230 m.collect_entries(page_size, &mut sizes, &mut mip_map, &mut include_full_size);
231 }
232 if let Some([min_size, max_size]) = mip_map {
233 let (first, mips) = Self::collect_mip_map(page_size, reduced_sizes, &sizes, min_size, max_size);
234 include_full_size |= first.is_some();
235 sizes.extend(first);
236 sizes.extend(mips);
237 }
238
239 sizes.sort_by_key(|s| s.width.0 * s.height.0);
240 sizes.dedup();
241
242 let full_downscale = if include_full_size { None } else { sizes.pop() };
243 sizes.reverse();
244
245 (full_downscale, sizes)
246 }
247 }
248 }
249
250 fn collect_mip_map(
251 page_size: PxSize,
252 reduced_sizes: &[PxSize],
253 entry_sizes: &[PxSize],
254 min_size: PxSize,
255 max_size: PxSize,
256 ) -> (Option<PxSize>, Vec<PxSize>) {
257 let page_downscale = fit_fill(page_size, max_size, true);
258 let mut size = page_downscale.unwrap_or(page_size) / Px(2);
259 let mut entries = vec![];
260 while min_size.width < size.width && min_size.height < size.height {
261 if let Some(entry) = fit_fill(page_size, size, true)
262 && !reduced_sizes.iter().any(|s| Self::near(entry, *s))
263 && !entry_sizes.iter().any(|s| Self::near(entry, *s))
264 {
265 entries.push(entry);
266 }
267 size /= Px(2);
268 }
269 (page_downscale, entries)
270 }
271 fn near(candidate: PxSize, existing: PxSize) -> bool {
272 let dist = (candidate - existing).abs();
273 dist.width < Px(10) && dist.height <= Px(10)
274 }
275
276 fn collect_entries(&self, page_size: PxSize, sizes: &mut Vec<PxSize>, mip_map: &mut Option<[PxSize; 2]>, include_full_size: &mut bool) {
277 match self {
278 ImageDownscaleMode::Fit(s) => match fit_fill(page_size, *s, false) {
279 Some(s) => sizes.push(s),
280 None => *include_full_size = true,
281 },
282 ImageDownscaleMode::Fill(s) => match fit_fill(page_size, *s, true) {
283 Some(s) => sizes.push(s),
284 None => *include_full_size = true,
285 },
286 ImageDownscaleMode::MipMap { min_size, max_size } => {
287 *include_full_size = true;
288 if let Some([min, max]) = mip_map {
289 *min = min.min(*min_size);
290 *max = max.min(*min_size);
291 } else {
292 *mip_map = Some([*min_size, *max_size]);
293 }
294 }
295 ImageDownscaleMode::Entries(modes) => {
296 for m in modes {
297 m.collect_entries(page_size, sizes, mip_map, include_full_size);
298 }
299 }
300 }
301 }
302}
303
304fn fit_fill(source_size: PxSize, new_size: PxSize, fill: bool) -> Option<PxSize> {
305 let source_size = source_size.cast::<f64>();
306 let new_size = new_size.cast::<f64>();
307
308 let w_ratio = new_size.width / source_size.width;
309 let h_ratio = new_size.height / source_size.height;
310
311 let ratio = if fill {
312 f64::max(w_ratio, h_ratio)
313 } else {
314 f64::min(w_ratio, h_ratio)
315 };
316
317 if ratio >= 1.0 {
318 return None;
319 }
320
321 let nw = u64::max((source_size.width * ratio).round() as _, 1);
322 let nh = u64::max((source_size.height * ratio).round() as _, 1);
323
324 const MAX: u64 = Px::MAX.0 as _;
325
326 let r = if nw > MAX {
327 let ratio = MAX as f64 / source_size.width;
328 (Px::MAX, Px(i32::max((source_size.height * ratio).round() as _, 1)))
329 } else if nh > MAX {
330 let ratio = MAX as f64 / source_size.height;
331 (Px(i32::max((source_size.width * ratio).round() as _, 1)), Px::MAX)
332 } else {
333 (Px(nw as _), Px(nh as _))
334 }
335 .into();
336
337 Some(r)
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342#[non_exhaustive]
343pub enum ImageDataFormat {
344 Bgra8 {
349 size: PxSize,
351 density: Option<PxDensity2d>,
353 original_color_type: ColorType,
355 },
356
357 A8 {
362 size: PxSize,
364 },
365
366 FileExtension(Txt),
371
372 MimeType(Txt),
377
378 Unknown,
382}
383impl From<Txt> for ImageDataFormat {
384 fn from(ext_or_mime: Txt) -> Self {
385 if ext_or_mime.contains('/') {
386 ImageDataFormat::MimeType(ext_or_mime)
387 } else {
388 ImageDataFormat::FileExtension(ext_or_mime)
389 }
390 }
391}
392impl From<&str> for ImageDataFormat {
393 fn from(ext_or_mime: &str) -> Self {
394 Txt::from_str(ext_or_mime).into()
395 }
396}
397impl From<PxSize> for ImageDataFormat {
398 fn from(bgra8_size: PxSize) -> Self {
399 ImageDataFormat::Bgra8 {
400 size: bgra8_size,
401 density: None,
402 original_color_type: ColorType::BGRA8,
403 }
404 }
405}
406impl PartialEq for ImageDataFormat {
407 fn eq(&self, other: &Self) -> bool {
408 match (self, other) {
409 (Self::FileExtension(l0), Self::FileExtension(r0)) => l0 == r0,
410 (Self::MimeType(l0), Self::MimeType(r0)) => l0 == r0,
411 (
412 Self::Bgra8 {
413 size: s0,
414 density: p0,
415 original_color_type: oc0,
416 },
417 Self::Bgra8 {
418 size: s1,
419 density: p1,
420 original_color_type: oc1,
421 },
422 ) => s0 == s1 && density_key(*p0) == density_key(*p1) && oc0 == oc1,
423 (Self::Unknown, Self::Unknown) => true,
424 _ => false,
425 }
426 }
427}
428impl Eq for ImageDataFormat {}
429impl std::hash::Hash for ImageDataFormat {
430 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
431 core::mem::discriminant(self).hash(state);
432 match self {
433 ImageDataFormat::Bgra8 {
434 size,
435 density,
436 original_color_type,
437 } => {
438 size.hash(state);
439 density_key(*density).hash(state);
440 original_color_type.hash(state)
441 }
442 ImageDataFormat::A8 { size } => {
443 size.hash(state);
444 }
445 ImageDataFormat::FileExtension(ext) => ext.hash(state),
446 ImageDataFormat::MimeType(mt) => mt.hash(state),
447 ImageDataFormat::Unknown => {}
448 }
449 }
450}
451
452fn density_key(density: Option<PxDensity2d>) -> Option<(u16, u16)> {
453 density.map(|s| ((s.width.ppcm() * 3.0) as u16, (s.height.ppcm() * 3.0) as u16))
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458#[non_exhaustive]
459pub struct ImageEntryMetadata {
460 pub parent: ImageId,
464 pub index: usize,
466 pub kind: ImageEntryKind,
468}
469impl ImageEntryMetadata {
470 pub fn new(parent: ImageId, index: usize, kind: ImageEntryKind) -> Self {
472 Self { parent, index, kind }
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
478#[non_exhaustive]
479pub struct ImageMetadata {
480 pub id: ImageId,
482 pub size: PxSize,
484 pub density: Option<PxDensity2d>,
486 pub is_mask: bool,
488 pub original_color_type: ColorType,
490 pub format_name: Txt,
492 pub parent: Option<ImageEntryMetadata>,
496
497 pub extensions: Vec<(ApiExtensionId, ApiExtensionPayload)>,
499}
500impl ImageMetadata {
501 pub fn new(id: ImageId, size: PxSize, is_mask: bool, original_color_type: ColorType) -> Self {
503 Self {
504 id,
505 size,
506 density: None,
507 is_mask,
508 original_color_type,
509 parent: None,
510 extensions: vec![],
511 format_name: Txt::default(),
512 }
513 }
514}
515impl Default for ImageMetadata {
516 fn default() -> Self {
517 Self {
518 id: ImageId::INVALID,
519 size: Default::default(),
520 density: Default::default(),
521 is_mask: Default::default(),
522 original_color_type: ColorType::BGRA8,
523 parent: Default::default(),
524 extensions: vec![],
525 format_name: Txt::default(),
526 }
527 }
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
532#[non_exhaustive]
533pub enum ImageEntryKind {
534 Page,
536 Reduced {
540 synthetic: bool,
542 },
543 Other {
545 kind: Txt,
549 },
550}
551impl ImageEntryKind {
552 fn discriminant(&self) -> u8 {
553 match self {
554 ImageEntryKind::Page => 0,
555 ImageEntryKind::Reduced { .. } => 1,
556 ImageEntryKind::Other { .. } => 2,
557 }
558 }
559}
560impl std::cmp::Ord for ImageEntryKind {
561 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
562 self.discriminant().cmp(&other.discriminant())
563 }
564}
565impl std::cmp::PartialOrd for ImageEntryKind {
566 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
567 Some(self.cmp(other))
568 }
569}
570
571#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
577#[non_exhaustive]
578pub struct ImageDecoded {
579 pub meta: ImageMetadata,
581
582 pub partial: Option<PartialImageKind>,
588
589 pub pixels: IpcBytes,
593 pub is_opaque: bool,
595}
596impl Default for ImageDecoded {
597 fn default() -> Self {
598 Self {
599 meta: Default::default(),
600 partial: Default::default(),
601 pixels: Default::default(),
602 is_opaque: true,
603 }
604 }
605}
606impl ImageDecoded {
607 pub fn new(meta: ImageMetadata, pixels: IpcBytes, is_opaque: bool) -> Self {
609 Self {
610 meta,
611 partial: None,
612 pixels,
613 is_opaque,
614 }
615 }
616}
617
618#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
620#[non_exhaustive]
621pub enum PartialImageKind {
622 Placeholder {
626 pixel_size: PxSize,
628 },
629 Rows {
633 y: Px,
637 height: Px,
639 },
640}
641
642bitflags! {
643 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
647 pub struct ImageFormatCapability: u32 {
648 const ENCODE = 1 << 0;
650 const DECODE_ENTRIES = 1 << 1;
652 const ENCODE_ENTRIES = (1 << 2) | ImageFormatCapability::ENCODE.bits();
654 const DECODE_PROGRESSIVE = 1 << 3;
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
666#[non_exhaustive]
667pub struct ImageFormat {
668 pub display_name: Txt,
670
671 pub media_type_suffixes: Txt,
675
676 pub file_extensions: Txt,
680
681 pub magic_numbers: Txt,
685
686 pub capabilities: ImageFormatCapability,
688}
689impl ImageFormat {
690 #[deprecated = "use `from_static2`, it will replace this function next breaking release"]
696 pub const fn from_static(
697 display_name: &'static str,
698 media_type_suffixes: &'static str,
699 file_extensions: &'static str,
700 capabilities: ImageFormatCapability,
701 ) -> Self {
702 assert!(media_type_suffixes.is_ascii());
703 Self {
704 display_name: Txt::from_static(display_name),
705 media_type_suffixes: Txt::from_static(media_type_suffixes),
706 file_extensions: Txt::from_static(file_extensions),
707 magic_numbers: Txt::from_static(""),
708 capabilities,
709 }
710 }
711
712 pub const fn from_static2(
718 display_name: &'static str,
719 media_type_suffixes: &'static str,
720 file_extensions: &'static str,
721 magic_numbers: &'static str,
722 capabilities: ImageFormatCapability,
723 ) -> Self {
724 assert!(media_type_suffixes.is_ascii());
725 assert!(magic_numbers.is_ascii());
726 Self {
727 display_name: Txt::from_static(display_name),
728 media_type_suffixes: Txt::from_static(media_type_suffixes),
729 file_extensions: Txt::from_static(file_extensions),
730 magic_numbers: Txt::from_static(magic_numbers),
731 capabilities,
732 }
733 }
734
735 pub fn media_type_suffixes_iter(&self) -> impl Iterator<Item = &str> {
737 self.media_type_suffixes.split(',').map(|e| e.trim())
738 }
739
740 pub fn media_types(&self) -> impl Iterator<Item = Txt> {
742 self.media_type_suffixes_iter().map(Txt::from_str)
743 }
744
745 pub fn file_extensions_iter(&self) -> impl Iterator<Item = &str> {
747 self.file_extensions.split(',').map(|e| e.trim())
748 }
749
750 pub fn matches(&self, f: &str) -> bool {
754 let f = f.strip_prefix('.').unwrap_or(f);
755 let f = f.strip_prefix("image/").unwrap_or(f);
756 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))
757 }
758
759 pub fn matches_magic(&self, file_prefix: &[u8]) -> bool {
763 'search: for magic in self.magic_numbers.split(',') {
764 if magic.is_empty() || magic.len() > file_prefix.len() * 2 {
765 continue 'search;
766 }
767 'm: for (c, b) in magic.as_bytes().chunks_exact(2).zip(file_prefix) {
768 if c == b"xx" {
769 continue 'm;
770 }
771 fn decode(c: u8) -> u8 {
772 if c >= b'a' { c - b'a' + 10 } else { c - b'0' }
773 }
774 let c = (decode(c[0]) << 4) | decode(c[1]);
775 if c != *b {
776 continue 'search;
777 }
778 }
779 return true;
780 }
781 false
782 }
783}
784
785#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
787#[non_exhaustive]
788pub struct ColorType {
789 pub name: Txt,
791 pub bits: u8,
793 pub channels: u8,
795}
796impl ColorType {
797 pub const fn new(name: Txt, bits: u8, channels: u8) -> Self {
799 Self { name, bits, channels }
800 }
801
802 pub fn bits_per_pixel(&self) -> u16 {
804 self.bits as u16 * self.channels as u16
805 }
806
807 pub fn bytes_per_pixel(&self) -> u16 {
809 self.bits_per_pixel() / 8
810 }
811}
812impl ColorType {
813 pub const BGRA8: ColorType = ColorType::new(Txt::from_static("BGRA8"), 8, 4);
815 pub const RGBA8: ColorType = ColorType::new(Txt::from_static("RGBA8"), 8, 4);
817
818 pub const A8: ColorType = ColorType::new(Txt::from_static("A8"), 8, 4);
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
824#[non_exhaustive]
825pub struct ImageEncodeRequest {
826 pub id: ImageId,
828
829 pub entries: Vec<(ImageId, ImageEntryKind)>,
833
834 pub format: Txt,
836}
837impl ImageEncodeRequest {
838 pub fn new(id: ImageId, format: Txt) -> Self {
840 Self {
841 id,
842 entries: vec![],
843 format,
844 }
845 }
846}