pyxel/
pyxel.rs

1use crate::{
2    deserialization::{
3        deserialize_as_degrees, deserialize_as_milliseconds, deserialize_map_as_vec,
4        deserialize_multipliers,
5    },
6    error::PyxelError,
7};
8
9use derivative::Derivative;
10use semver::Version;
11use serde::Deserialize;
12use std::{collections::BTreeMap, time::Duration};
13
14/// An RGBA color
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct Color {
17    /// The red component of this color.
18    pub r: u8,
19    /// The green component of this color.
20    pub g: u8,
21    /// The blue component of this color.
22    pub b: u8,
23    /// The alpha component of this color.
24    pub a: u8,
25}
26
27impl std::str::FromStr for Color {
28    type Err = hex::FromHexError;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        use hex::FromHex;
32
33        let decoded = <[u8; 4]>::from_hex(s)?;
34
35        Ok(Color {
36            r: decoded[1],
37            g: decoded[2],
38            b: decoded[3],
39            a: decoded[0],
40        })
41    }
42}
43
44impl<'de> serde::de::Deserialize<'de> for Color {
45    fn deserialize<D: serde::de::Deserializer<'de>>(deserializer: D) -> Result<Color, D::Error> {
46        deserializer.deserialize_str(ColorVisitor)
47    }
48}
49
50struct ColorVisitor;
51
52impl<'de> serde::de::Visitor<'de> for ColorVisitor {
53    type Value = Color;
54
55    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
56        formatter.write_str("a color in the format AARRGGBB")
57    }
58
59    fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
60        use std::str::FromStr;
61        Self::Value::from_str(value).map_err(serde::de::Error::custom)
62    }
63}
64
65/// A Pyxel palette.
66#[derive(Debug, Deserialize)]
67pub struct Palette {
68    #[serde(deserialize_with = "deserialize_map_as_vec")]
69    colors: Vec<Option<Color>>,
70
71    height: u8,
72
73    #[serde(rename = "numColors")]
74    num_colors: usize,
75
76    width: u8,
77}
78
79impl Palette {
80    /// Returns the colors that make up this palette.
81    pub fn colors(&self) -> &Vec<Option<Color>> {
82        &self.colors
83    }
84
85    /// Returns the height of this palette when displayed in the PyxelEdit UI.
86    pub fn height(&self) -> u8 {
87        self.height
88    }
89
90    /// Returns the width of this palette when displayed in the PyxelEdit UI.
91    pub fn width(&self) -> u8 {
92        self.width
93    }
94}
95
96/// A reference to a tile in a Pyxel tileset.
97#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
98pub struct TileRef {
99    index: usize,
100    #[serde(deserialize_with = "deserialize_as_degrees")]
101    rot: f64,
102
103    #[serde(rename = "flipX")]
104    flip_x: bool,
105}
106
107impl TileRef {
108    /// Returns the index of the tile in the tileset.
109    pub fn index(&self) -> usize {
110        self.index
111    }
112
113    /// Returns the rotation of this tile in degrees.
114    pub fn rot(&self) -> f64 {
115        self.rot
116    }
117
118    /// Returns `true` if the tile is flipped horizontally.
119    pub fn flip_x(&self) -> bool {
120        self.flip_x
121    }
122}
123
124/// A Pyxel blend mode.
125#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
126pub enum BlendMode {
127    /// Normal blend mode
128    #[serde(rename = "normal")]
129    Normal,
130
131    /// Multiply blend mode
132    #[serde(rename = "multiply")]
133    Multiply,
134
135    /// Add blend mode
136    #[serde(rename = "add")]
137    Add,
138
139    /// Difference blend mode
140    #[serde(rename = "difference")]
141    Difference,
142
143    /// Darken blend mode
144    #[serde(rename = "darken")]
145    Darken,
146
147    /// Lighten blend mode
148    #[serde(rename = "lighten")]
149    Lighten,
150
151    /// Hard light blend mode
152    #[serde(rename = "hardlight")]
153    Hardlight,
154
155    /// Invert blend mode
156    #[serde(rename = "invert")]
157    Invert,
158
159    /// Overlay blend mode
160    #[serde(rename = "overlay")]
161    Overlay,
162
163    /// Screen blend mode
164    #[serde(rename = "screen")]
165    Screen,
166
167    /// Subtract blend mode
168    #[serde(rename = "subtract")]
169    Subtract,
170}
171
172#[cfg(feature = "images")]
173fn default_image() -> image::DynamicImage {
174    image::DynamicImage::new_rgba8(1, 1)
175}
176
177/// A Pyxel canvas layer.
178#[derive(Derivative, Deserialize)]
179#[derivative(Debug)]
180pub struct Layer {
181    alpha: u8,
182
183    #[serde(rename = "blendMode")]
184    blend_mode: BlendMode,
185
186    hidden: bool,
187    muted: bool,
188    name: String,
189    soloed: bool,
190
191    #[serde(rename = "tileRefs")]
192    tile_refs: BTreeMap<usize, TileRef>,
193
194    #[cfg(not(feature = "images"))]
195    #[serde(skip)]
196    image_data: Vec<u8>,
197
198    #[cfg(feature = "images")]
199    #[derivative(Debug = "ignore")]
200    #[serde(default = "default_image", skip)]
201    image: image::DynamicImage,
202}
203
204impl Layer {
205    /// Returns the alpha value of this layer.
206    pub fn alpha(&self) -> u8 {
207        self.alpha
208    }
209
210    /// Returns the blend mode for this layer.
211    pub fn blend_mode(&self) -> BlendMode {
212        self.blend_mode
213    }
214
215    /// Returns `true` if this layer is hidden in the PyxelEdit UI.
216    pub fn hidden(&self) -> bool {
217        self.hidden
218    }
219
220    /// Returns `true` if this layer is muted in the PyxelEdit UI.
221    pub fn muted(&self) -> bool {
222        self.muted
223    }
224
225    /// Returns the name of this layer.
226    pub fn name(&self) -> &String {
227        &self.name
228    }
229
230    /// Returns `true` if this layer is soloed in the PyxelEdit UI.
231    pub fn soloed(&self) -> bool {
232        self.soloed
233    }
234
235    /// Returns the tilerefs for this layer.
236    pub fn tile_refs(&self) -> &BTreeMap<usize, TileRef> {
237        &self.tile_refs
238    }
239
240    /// Returns the raw bytes of the image for this layer.
241    #[cfg(not(feature = "images"))]
242    pub fn image_data(&self) -> &Vec<u8> {
243        &self.image_data
244    }
245
246    /// Returns the image for this layer.
247    #[cfg(feature = "images")]
248    pub fn image(&self) -> &image::DynamicImage {
249        &self.image
250    }
251}
252
253/// A Pyxel canvas.
254#[derive(Debug, Deserialize)]
255pub struct Canvas {
256    #[serde(deserialize_with = "deserialize_map_as_vec")]
257    layers: Vec<Layer>,
258    height: i32,
259
260    #[serde(rename = "numLayers")]
261    num_layers: usize,
262
263    #[serde(rename = "tileHeight")]
264    tile_height: u16,
265
266    #[serde(rename = "tileWidth")]
267    tile_width: u16,
268
269    width: i32,
270}
271
272impl Canvas {
273    /// Returns the layers of this canvas.
274    pub fn layers(&self) -> &Vec<Layer> {
275        &self.layers
276    }
277
278    /// Returns the height of this canvas in pixels.
279    pub fn height(&self) -> i32 {
280        self.height
281    }
282
283    /// Returns the height of the tiles in this canvas in pixels.
284    pub fn tile_height(&self) -> u16 {
285        self.tile_height
286    }
287
288    /// Returns the width of the tiles in this canvas in pixels.
289    pub fn tile_width(&self) -> u16 {
290        self.tile_width
291    }
292
293    /// Returns the width of this canvas in pixels.
294    pub fn width(&self) -> i32 {
295        self.width
296    }
297}
298
299/// A Pyxel tileset.
300#[derive(Derivative, Deserialize)]
301#[derivative(Debug)]
302pub struct Tileset {
303    #[serde(rename = "fixedWidth")]
304    fixed_width: bool,
305
306    #[serde(rename = "numTiles")]
307    num_tiles: usize,
308
309    #[serde(rename = "tileHeight")]
310    tile_height: u16,
311
312    #[serde(rename = "tileWidth")]
313    tile_width: u16,
314
315    #[serde(rename = "tilesWide")]
316    tiles_wide: u8,
317
318    #[cfg(not(feature = "images"))]
319    #[serde(skip)]
320    image_data: Vec<Vec<u8>>,
321
322    #[cfg(feature = "images")]
323    #[derivative(Debug = "ignore")]
324    #[serde(skip)]
325    images: Vec<image::DynamicImage>,
326}
327
328impl Tileset {
329    /// Returns `true` if this tileset is fixed width when displayed in the PyxelEdit UI.
330    pub fn fixed_width(&self) -> bool {
331        self.fixed_width
332    }
333
334    /// Returns the tile height in pixels of the tiles in this tileset.
335    pub fn tile_height(&self) -> u16 {
336        self.tile_height
337    }
338
339    /// Returns the tile width in pixels of the tiles in this tileset.
340    pub fn tile_width(&self) -> u16 {
341        self.tile_width
342    }
343
344    /// Returns the width of this tileset when displayed in the PyxelEdit UI.
345    pub fn tiles_wide(&self) -> u8 {
346        self.tiles_wide
347    }
348
349    /// Returns raw bytes of the images for the tiles in this tileset.
350    #[cfg(not(feature = "images"))]
351    pub fn image_data(&self) -> &Vec<Vec<u8>> {
352        &self.image_data
353    }
354
355    /// Returns the images for the tiles in this tileset.
356    #[cfg(feature = "images")]
357    pub fn images(&self) -> &Vec<image::DynamicImage> {
358        &self.images
359    }
360}
361
362/// A Pyxel animation.
363#[derive(Debug, Deserialize)]
364pub struct Animation {
365    #[serde(rename = "baseTile")]
366    base_tile: usize,
367
368    #[serde(
369        deserialize_with = "deserialize_as_milliseconds",
370        rename = "frameDuration"
371    )]
372    frame_duration: Duration,
373
374    #[serde(
375        deserialize_with = "deserialize_multipliers",
376        rename = "frameDurationMultipliers"
377    )]
378    frame_duration_multipliers: Vec<f64>,
379
380    length: usize,
381    name: String,
382}
383
384impl Animation {
385    /// Returns the canvas tile this animation starts on.
386    pub fn base_tile(&self) -> usize {
387        self.base_tile
388    }
389
390    /// Returns the base frame duration for this animation.
391    pub fn frame_duration(&self) -> Duration {
392        self.frame_duration
393    }
394
395    /// Returns the frame duration multipliers for this animation.
396    pub fn frame_duration_multipliers(&self) -> &Vec<f64> {
397        &self.frame_duration_multipliers
398    }
399
400    /// Returns the number of frames in this animation.
401    pub fn length(&self) -> usize {
402        self.length
403    }
404
405    /// Returns the name of this animation.
406    pub fn name(&self) -> &String {
407        &self.name
408    }
409}
410
411/// A Pyxel document.
412#[derive(Debug, Deserialize)]
413pub struct Pyxel {
414    #[serde(deserialize_with = "deserialize_map_as_vec")]
415    animations: Vec<Animation>,
416    canvas: Canvas,
417    name: String,
418    palette: Palette,
419    tileset: Tileset,
420    version: Version,
421}
422
423impl Pyxel {
424    /// Returns the animations for this document.
425    pub fn animations(&self) -> &Vec<Animation> {
426        &self.animations
427    }
428
429    /// Returns the canvas for this document.
430    pub fn canvas(&self) -> &Canvas {
431        &self.canvas
432    }
433
434    /// Returns the name of this document.
435    pub fn name(&self) -> &String {
436        &self.name
437    }
438
439    /// Returns the palette for this document.
440    pub fn palette(&self) -> &Palette {
441        &self.palette
442    }
443
444    /// Returns the tileset for this document.
445    pub fn tileset(&self) -> &Tileset {
446        &self.tileset
447    }
448
449    /// Returns the version of PyxelEdit this document was created with.
450    pub fn version(&self) -> &Version {
451        &self.version
452    }
453}
454
455#[cfg(not(feature = "images"))]
456fn load_image_data_from_zip<R: std::io::Read + std::io::Seek>(
457    zip: &mut zip::ZipArchive<R>,
458    path: &str,
459) -> Result<Vec<u8>, PyxelError> {
460    use std::io::Read;
461
462    let mut file = zip.by_name(path)?;
463
464    let mut buf = Vec::new();
465    file.read_to_end(&mut buf)?;
466
467    Ok(buf)
468}
469
470#[cfg(feature = "images")]
471fn load_image_from_zip<R: std::io::Read + std::io::Seek>(
472    zip: &mut zip::ZipArchive<R>,
473    path: &str,
474) -> Result<image::DynamicImage, PyxelError> {
475    use std::io::Read;
476
477    let mut file = zip.by_name(path)?;
478
479    let mut buf = Vec::new();
480    file.read_to_end(&mut buf)?;
481
482    let image = image::load_from_memory_with_format(&buf, image::ImageFormat::PNG)?;
483    Ok(image)
484}
485
486/// Load a Pyxel document from a reader.
487///
488/// # Examples
489///
490/// ```
491/// use std::fs::File;
492/// # fn main() -> Result<(), pyxel::PyxelError> {
493/// let file = File::open("resources/doc.pyxel")?;
494/// let doc = pyxel::load(file)?;
495/// # Ok(())
496/// # }
497/// ```
498pub fn load<R: std::io::Read + std::io::Seek>(r: R) -> Result<Pyxel, PyxelError> {
499    let mut archive = zip::ZipArchive::new(r)?;
500    let data = archive.by_name("docData.json")?;
501
502    let mut pyxel: Pyxel = serde_json::from_reader(data)?;
503
504    for i in 0..pyxel.canvas().num_layers {
505        #[cfg(not(feature = "images"))]
506        {
507            let image_data = load_image_data_from_zip(&mut archive, &format!("layer{}.png", i))?;
508            pyxel.canvas.layers[i].image_data = image_data;
509        }
510        #[cfg(feature = "images")]
511        {
512            let image = load_image_from_zip(&mut archive, &format!("layer{}.png", i))?;
513            pyxel.canvas.layers[i].image = image;
514        }
515    }
516
517    for i in 0..pyxel.tileset().num_tiles {
518        #[cfg(not(feature = "images"))]
519        {
520            let image_data = load_image_data_from_zip(&mut archive, &format!("layer{}.png", i))?;
521            pyxel.tileset.image_data.insert(i, image_data);
522        }
523        #[cfg(feature = "images")]
524        {
525            let image = load_image_from_zip(&mut archive, &format!("tile{}.png", i))?;
526            pyxel.tileset.images.insert(i, image);
527        }
528    }
529
530    Ok(pyxel)
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536    use std::{collections::BTreeMap, fs::File, str::FromStr};
537
538    #[test]
539    fn convert_color_from_aarrggbb() {
540        let c = Color::from_str("ffaabbcc").unwrap();
541        assert_eq!(170, c.r);
542        assert_eq!(187, c.g);
543        assert_eq!(204, c.b);
544        assert_eq!(255, c.a);
545    }
546
547    const TEST_FILE: &str = "resources/test_v0.4.8.pyxel";
548
549    #[test]
550    fn load_palette_colors() {
551        let file = File::open(TEST_FILE).unwrap();
552        let doc = load(file).unwrap();
553
554        assert_eq!(
555            &vec![
556                Some(Color {
557                    r: 190,
558                    g: 53,
559                    b: 53,
560                    a: 255
561                }),
562                Some(Color {
563                    r: 249,
564                    g: 155,
565                    b: 151,
566                    a: 255
567                }),
568                Some(Color {
569                    r: 145,
570                    g: 95,
571                    b: 51,
572                    a: 255
573                }),
574                Some(Color {
575                    r: 209,
576                    g: 127,
577                    b: 48,
578                    a: 255
579                }),
580                Some(Color {
581                    r: 247,
582                    g: 238,
583                    b: 89,
584                    a: 255
585                }),
586                Some(Color {
587                    r: 89,
588                    g: 205,
589                    b: 54,
590                    a: 255
591                }),
592                Some(Color {
593                    r: 131,
594                    g: 240,
595                    b: 220,
596                    a: 255
597                }),
598                Some(Color {
599                    r: 117,
600                    g: 161,
601                    b: 236,
602                    a: 255
603                }),
604                Some(Color {
605                    r: 65,
606                    g: 55,
607                    b: 205,
608                    a: 255
609                }),
610                Some(Color {
611                    r: 204,
612                    g: 89,
613                    b: 198,
614                    a: 255
615                }),
616                Some(Color {
617                    r: 255,
618                    g: 255,
619                    b: 255,
620                    a: 255
621                }),
622                Some(Color {
623                    r: 202,
624                    g: 202,
625                    b: 202,
626                    a: 255
627                }),
628                Some(Color {
629                    r: 142,
630                    g: 142,
631                    b: 142,
632                    a: 255
633                }),
634                Some(Color {
635                    r: 91,
636                    g: 91,
637                    b: 91,
638                    a: 255
639                }),
640                Some(Color {
641                    r: 0,
642                    g: 0,
643                    b: 0,
644                    a: 255
645                })
646            ],
647            doc.palette().colors()
648        );
649    }
650
651    #[test]
652    fn load_canvas_layer_tilerefs() {
653        let file = File::open(TEST_FILE).unwrap();
654        let doc = load(file).unwrap();
655
656        let mut tile_refs = BTreeMap::new();
657        tile_refs.insert(
658            56,
659            TileRef {
660                index: 0,
661                rot: 0.0,
662                flip_x: false,
663            },
664        );
665        tile_refs.insert(
666            57,
667            TileRef {
668                index: 0,
669                rot: 90.0,
670                flip_x: false,
671            },
672        );
673        tile_refs.insert(
674            58,
675            TileRef {
676                index: 0,
677                rot: 180.0,
678                flip_x: false,
679            },
680        );
681        tile_refs.insert(
682            59,
683            TileRef {
684                index: 0,
685                rot: 270.0,
686                flip_x: false,
687            },
688        );
689        tile_refs.insert(
690            60,
691            TileRef {
692                index: 0,
693                rot: 0.0,
694                flip_x: true,
695            },
696        );
697        tile_refs.insert(
698            61,
699            TileRef {
700                index: 0,
701                rot: 90.0,
702                flip_x: true,
703            },
704        );
705        tile_refs.insert(
706            62,
707            TileRef {
708                index: 0,
709                rot: 180.0,
710                flip_x: true,
711            },
712        );
713        tile_refs.insert(
714            63,
715            TileRef {
716                index: 0,
717                rot: 270.0,
718                flip_x: true,
719            },
720        );
721
722        assert_eq!(&tile_refs, doc.canvas().layers()[1].tile_refs());
723    }
724}