1use crate::prelude::*;
18use serde::{Deserialize, Serialize};
19
20use alloc::collections::BTreeMap;
21
22use crate::bitmap::Bitmap;
23use crate::bundle::Bundle;
24use crate::error::ContentError;
25use crate::format::{AssetKind, AssetPath, DecodeCx, Format, Formats};
26
27pub const MAX_SIDE: u32 = 4096;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct ImageInfo {
36 pub width: u32,
38 pub height: u32,
40}
41
42impl ImageInfo {
43 #[must_use]
46 pub const fn new(width: u32, height: u32) -> Self {
47 Self { width, height }
48 }
49
50 pub fn checked(width: u32, height: u32) -> Result<Self, String> {
53 if width == 0 || height == 0 {
54 return Err(format!(
55 "image is {width}×{height} px; both sides must be greater than 0"
56 ));
57 }
58 if width > MAX_SIDE || height > MAX_SIDE {
59 return Err(format!(
60 "image is {width}×{height} px; neither side may be over {MAX_SIDE}"
61 ));
62 }
63 Ok(Self { width, height })
64 }
65}
66
67impl AssetKind for ImageInfo {
68 const NAME: &'static str = "image";
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
74pub struct Image {
75 pub width: u32,
77 pub height: u32,
79 pub rgba: Vec<u8>,
81}
82
83impl Image {
84 pub fn filled(width: u32, height: u32, color: impl Into<[u8; 4]>) -> Self {
87 let color = color.into();
88 Self {
89 width,
90 height,
91 rgba: color.repeat(width as usize * height as usize),
92 }
93 }
94
95 pub fn pixel(&self, x: u32, y: u32) -> Option<[u8; 4]> {
97 if x >= self.width || y >= self.height {
98 return None;
99 }
100 let i = (y as usize * self.width as usize + x as usize) * 4;
101 self.rgba.get(i..i + 4)?.try_into().ok()
102 }
103
104 pub fn info(&self) -> ImageInfo {
106 ImageInfo {
107 width: self.width,
108 height: self.height,
109 }
110 }
111}
112
113impl AssetKind for Image {
114 const NAME: &'static str = "image pixels";
115}
116
117pub const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
119
120pub const MAX_PNG_BYTES: usize = 64 << 20;
123
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub struct PngInfo;
128
129impl Format for PngInfo {
130 type Asset = ImageInfo;
131
132 fn id(&self) -> &'static str {
133 "png"
134 }
135
136 fn extensions(&self) -> &'static [&'static str] {
137 &["png"]
138 }
139
140 fn sniff(&self, bytes: &[u8]) -> bool {
141 bytes.starts_with(PNG_SIGNATURE)
142 }
143
144 fn max_bytes(&self) -> usize {
145 MAX_PNG_BYTES
146 }
147
148 fn decode(
149 &self,
150 file: &AssetPath,
151 bytes: &[u8],
152 _cx: &DecodeCx<'_>,
153 ) -> Result<ImageInfo, Vec<ContentError>> {
154 png_info(bytes).map_err(|message| vec![ContentError::new(file.shown(), message)])
155 }
156}
157
158fn png_info(bytes: &[u8]) -> Result<ImageInfo, String> {
160 let (width, height) = png_size(bytes).ok_or("not a valid PNG file")?;
161 ImageInfo::checked(width, height)
162}
163
164#[cfg(feature = "png")]
168#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
169pub struct Png;
170
171#[cfg(feature = "png")]
172impl Format for Png {
173 type Asset = Image;
174
175 fn id(&self) -> &'static str {
176 "png"
177 }
178
179 fn extensions(&self) -> &'static [&'static str] {
180 &["png"]
181 }
182
183 fn sniff(&self, bytes: &[u8]) -> bool {
184 bytes.starts_with(PNG_SIGNATURE)
185 }
186
187 fn max_bytes(&self) -> usize {
188 MAX_PNG_BYTES
189 }
190
191 fn decode(
192 &self,
193 file: &AssetPath,
194 bytes: &[u8],
195 _cx: &DecodeCx<'_>,
196 ) -> Result<Image, Vec<ContentError>> {
197 let error = |message: String| vec![ContentError::new(file.shown(), message)];
198 png_info(bytes).map_err(error)?;
199 let (width, height, rgba) = crate::bitmap::convert::png_pixels(bytes).map_err(error)?;
200 Ok(Image {
201 width,
202 height,
203 rgba,
204 })
205 }
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
212pub struct ImageId(&'static str);
213
214impl ImageId {
215 pub const fn path(self) -> &'static str {
217 self.0
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223pub struct ImageRect {
224 pub x: u32,
226 pub y: u32,
228 pub w: u32,
230 pub h: u32,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
236pub struct Picture {
237 pub image: ImageId,
239 pub rect: ImageRect,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Default)]
247#[non_exhaustive]
248pub struct ImageTable {
249 pub images: BTreeMap<&'static str, ImageInfo>,
251}
252
253impl ImageTable {
254 #[must_use]
256 pub const fn new(images: BTreeMap<&'static str, ImageInfo>) -> Self {
257 Self { images }
258 }
259
260 pub fn load(bundle: &Bundle, skip: &[&str]) -> Result<Self, Vec<ContentError>> {
264 let files = bundle
265 .files_under("")
266 .into_iter()
267 .filter(|path| is_png(path) && !skip.contains(path))
268 .map(|path| (path, bundle.bytes(path).unwrap_or_default()));
269 Self::from_files(bundle, files)
270 }
271
272 pub fn load_with(
278 bundle: &Bundle,
279 formats: &Formats,
280 skip: &[&str],
281 ) -> Result<Self, Vec<ContentError>> {
282 let files = bundle
283 .files_under("")
284 .into_iter()
285 .filter(|path| formats.claims::<ImageInfo>(path) && !skip.contains(path))
286 .map(|path| (path, bundle.bytes(path).unwrap_or_default()));
287 Self::from_files_with(bundle, formats, files)
288 }
289
290 pub fn from_files_with<'a>(
295 bundle: &Bundle,
296 formats: &Formats,
297 files: impl IntoIterator<Item = (&'static str, &'a [u8])>,
298 ) -> Result<Self, Vec<ContentError>> {
299 let cx = DecodeCx::in_bundle(bundle).with_limits(formats.limits);
300 let mut images = BTreeMap::new();
301 let mut errors = Vec::new();
302 for (path, bytes) in files {
303 let file = AssetPath::in_bundle(bundle, path);
304 match formats.decode::<ImageInfo>(&file, bytes, &cx) {
305 Ok(info) => {
306 images.insert(path, info);
307 }
308 Err(e) => errors.extend(e),
309 }
310 }
311 if errors.is_empty() {
312 Ok(Self { images })
313 } else {
314 Err(errors)
315 }
316 }
317
318 pub fn from_files<'a>(
323 bundle: &Bundle,
324 files: impl IntoIterator<Item = (&'static str, &'a [u8])>,
325 ) -> Result<Self, Vec<ContentError>> {
326 let mut images = BTreeMap::new();
327 let mut errors = Vec::new();
328 for (path, bytes) in files {
329 match read_info(bytes) {
330 Ok(info) => {
331 images.insert(path, info);
332 }
333 Err(message) => {
334 errors.push(ContentError::new(bundle.display_path(path), message));
335 }
336 }
337 }
338 if errors.is_empty() {
339 Ok(Self { images })
340 } else {
341 Err(errors)
342 }
343 }
344
345 pub fn id(&self, path: &str) -> Option<ImageId> {
347 self.images
348 .get_key_value(path)
349 .map(|(&path, _)| ImageId(path))
350 }
351
352 pub fn info(&self, id: ImageId) -> Option<ImageInfo> {
354 self.images.get(id.path()).copied()
355 }
356
357 pub fn ids(&self) -> impl Iterator<Item = ImageId> + '_ {
359 self.images.keys().map(|&path| ImageId(path))
360 }
361}
362
363fn is_png(path: &str) -> bool {
365 path.rsplit_once('.').is_some_and(|(stem, ext)| {
366 !stem.is_empty() && !stem.ends_with('/') && ext.eq_ignore_ascii_case("png")
367 })
368}
369
370fn read_info(bytes: &[u8]) -> Result<ImageInfo, String> {
374 if Bitmap::is_bitmap(bytes) {
375 crate::bitmap::source_info(bytes)
376 } else {
377 png_info(bytes)
378 }
379}
380
381pub fn png_size(bytes: &[u8]) -> Option<(u32, u32)> {
383 let header = bytes.get(..24)?;
385 if &header[..8] != PNG_SIGNATURE || &header[12..16] != b"IHDR" {
386 return None;
387 }
388 let be = |at: usize| {
389 u32::from_be_bytes([header[at], header[at + 1], header[at + 2], header[at + 3]])
390 };
391 Some((be(16), be(20)))
392}
393
394#[cfg(test)]
396pub(crate) fn png_header(w: u32, h: u32) -> Vec<u8> {
397 let mut v = b"\x89PNG\r\n\x1a\n\0\0\0\x0dIHDR".to_vec();
398 v.extend(w.to_be_bytes());
399 v.extend(h.to_be_bytes());
400 v.extend([8, 6, 0, 0, 0]);
401 v
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::bundle::Layer;
408
409 fn bundle(files: &'static [(&'static str, &'static [u8])]) -> Bundle {
411 Bundle::new(vec![Layer::table("base", files)])
412 }
413
414 fn leaked(bytes: Vec<u8>) -> &'static [u8] {
415 Box::leak(bytes.into_boxed_slice())
416 }
417
418 fn messages(result: Result<ImageTable, Vec<ContentError>>) -> Vec<String> {
419 result
420 .err()
421 .unwrap_or_default()
422 .iter()
423 .map(ToString::to_string)
424 .collect()
425 }
426
427 #[test]
428 fn png_size_reads_header() {
429 assert_eq!(png_size(&png_header(256, 288)), Some((256, 288)));
430 assert_eq!(
432 png_size(&png_header(0x0102_0304, 0x0506_0708)),
433 Some((0x0102_0304, 0x0506_0708))
434 );
435 assert_eq!(png_size(&png_header(256, 288)[..23]), None);
436 let mut bad = png_header(1, 1);
437 bad[0] = 0;
438 assert_eq!(png_size(&bad), None);
439 let mut bad = png_header(1, 1);
440 bad[12] = b'X';
441 assert_eq!(png_size(&bad), None);
442 }
443
444 #[test]
445 fn is_png_goes_by_the_extension() {
446 assert!(is_png("images/a.png"));
447 assert!(is_png("a.PNG"));
448 assert!(!is_png("images/a.ron"));
449 assert!(!is_png("images/png"));
450 assert!(!is_png("images/.png"));
451 assert!(!is_png("a.png.txt"));
452 }
453
454 #[test]
455 fn from_files_keeps_each_size_by_path() {
456 let (a, b) = (png_header(16, 24), png_header(MAX_SIDE, MAX_SIDE));
458 let files = [("x/b.png", &b[..]), ("a.png", &a[..])];
459 let table = ImageTable::from_files(&bundle(&[]), files).unwrap_or_default();
460 let sizes: Vec<_> = table.images.iter().collect();
461 assert_eq!(
462 sizes,
463 [
464 (
465 &"a.png",
466 &ImageInfo {
467 width: 16,
468 height: 24
469 }
470 ),
471 (
472 &"x/b.png",
473 &ImageInfo {
474 width: MAX_SIDE,
475 height: MAX_SIDE
476 }
477 ),
478 ]
479 );
480 assert_eq!(
481 ImageTable::from_files(&bundle(&[]), []),
482 Ok(ImageTable::default())
483 );
484 }
485
486 #[test]
487 fn ids_name_images_by_path() {
488 let png = png_header(16, 24);
489 let files = [("a.png", &png[..]), ("b.png", &png[..])];
490 let table = ImageTable::from_files(&bundle(&[]), files).unwrap_or_default();
491 let a = table.id("a.png");
492 assert_eq!(a.map(ImageId::path), Some("a.png"));
493 assert_ne!(a, table.id("b.png"));
494 assert_eq!(table.id("c.png"), None);
495 let size = ImageInfo {
496 width: 16,
497 height: 24,
498 };
499 assert_eq!(a.and_then(|id| table.info(id)), Some(size));
500 let paths: Vec<_> = table.ids().map(ImageId::path).collect();
501 assert_eq!(paths, ["a.png", "b.png"]);
502 assert_eq!(a.and_then(|id| ImageTable::default().info(id)), None);
504 }
505
506 #[test]
507 fn a_one_bit_picture_in_a_pngs_place_has_the_pngs_size() {
508 let bits = crate::bitmap::encode((4, 2), (2, 1), &[&[0, 0]], None).unwrap();
510 let mut broken = bits.clone();
511 broken.pop();
512 let files = [("a.png", &bits[..]), ("b.png", &broken[..])];
513 let table = ImageTable::from_files(&bundle(&[]), files[..1].iter().copied());
514 let a = table.as_ref().ok().and_then(|t| t.id("a.png"));
515 let size = a.and_then(|id| table.as_ref().ok()?.info(id));
516 assert_eq!(
517 size,
518 Some(ImageInfo {
519 width: 2,
520 height: 1
521 })
522 );
523 assert_eq!(
524 messages(ImageTable::from_files(&bundle(&[]), files)),
525 ["base/b.png: a one-bit picture's size doesn't match its header"]
526 );
527 }
528
529 #[test]
530 fn every_bad_file_is_reported_by_name() {
531 let ok = png_header(1, 1);
532 let (wide, tall) = (png_header(MAX_SIDE + 1, 8), png_header(8, MAX_SIDE + 1));
533 let (flat, thin) = (png_header(8, 0), png_header(0, 8));
534 let files = [
535 ("images/ok.png", &ok[..]),
536 ("images/text.png", &b"not an image"[..]),
537 ("images/wide.png", &wide[..]),
538 ("images/tall.png", &tall[..]),
539 ("images/flat.png", &flat[..]),
540 ("images/thin.png", &thin[..]),
541 ];
542 assert_eq!(
543 messages(ImageTable::from_files(&bundle(&[]), files)),
544 [
545 "base/images/text.png: not a valid PNG file",
546 "base/images/wide.png: image is 4097×8 px; neither side may be over 4096",
547 "base/images/tall.png: image is 8×4097 px; neither side may be over 4096",
548 "base/images/flat.png: image is 8×0 px; both sides must be greater than 0",
549 "base/images/thin.png: image is 0×8 px; both sides must be greater than 0",
550 ]
551 );
552 }
553
554 #[test]
555 fn load_reads_every_png_of_the_bundle_but_the_skipped() {
556 let files: &'static [(&'static str, &'static [u8])] = Box::leak(Box::new([
557 ("data/a.ron", &b"()"[..]),
558 ("fonts/atlas.png", leaked(png_header(128, 64))),
559 ("images/card.png", leaked(png_header(16, 16))),
560 ("units/Big.PNG", leaked(png_header(32, 48))),
561 ]));
562 let bundle = bundle(files);
563 let table = ImageTable::load(&bundle, &["fonts/atlas.png"]).unwrap_or_default();
564 let paths: Vec<_> = table.ids().map(ImageId::path).collect();
565 assert_eq!(paths, ["images/card.png", "units/Big.PNG"]);
566 let all = ImageTable::load(&bundle, &[]).unwrap_or_default();
567 assert_eq!(all.images.len(), 3);
568 let broken: &'static [(&'static str, &'static [u8])] =
569 &[("images/bad.png", b"not an image")];
570 assert_eq!(
571 messages(ImageTable::load(&self::bundle(broken), &[])),
572 ["base/images/bad.png: not a valid PNG file"]
573 );
574 }
575
576 #[test]
577 fn filled_gives_the_colour_in_every_pixel() {
578 assert_eq!(Image::filled(0, 0, [1, 2, 3, 4]), Image::default());
579 let image = Image::filled(3, 2, [1, 2, 3, 4]);
580 assert_eq!((image.width, image.height), (3, 2));
581 assert_eq!(image.rgba, [1, 2, 3, 4].repeat(6));
582 assert_eq!(Image::filled(5, 0, [9; 4]).rgba, Vec::<u8>::new());
584 }
585
586 #[test]
587 fn pixel_reads_inside_and_nothing_outside() {
588 let mut image = Image::filled(3, 2, [0; 4]);
589 image.rgba[4..8].copy_from_slice(&[1, 2, 3, 4]);
590 image.rgba[20..24].copy_from_slice(&[5, 6, 7, 8]);
591 assert_eq!(image.pixel(1, 0), Some([1, 2, 3, 4]));
592 assert_eq!(image.pixel(0, 0), Some([0; 4]));
593 assert_eq!(image.pixel(2, 1), Some([5, 6, 7, 8]));
595 assert_eq!(image.pixel(3, 0), None);
597 assert_eq!(image.pixel(0, 2), None);
598 assert_eq!(image.pixel(u32::MAX, 0), None);
600 assert_eq!(image.pixel(0, u32::MAX), None);
601 image.rgba.truncate(20);
603 assert_eq!(image.pixel(2, 1), None);
604 }
605
606 const SIZE: ImageInfo = ImageInfo::new(16, 8);
608 const NONE: ImageTable = ImageTable::new(BTreeMap::new());
609
610 #[test]
611 fn the_constructors_give_the_literals() {
612 assert_eq!(
613 SIZE,
614 ImageInfo {
615 width: 16,
616 height: 8,
617 }
618 );
619 assert_eq!(ImageInfo::checked(16, 8), Ok(SIZE));
620 assert_eq!(NONE, ImageTable::default());
621 assert_eq!(
622 ImageTable::new([("a.png", SIZE)].into()),
623 ImageTable {
624 images: [("a.png", SIZE)].into(),
625 }
626 );
627 }
628}