paperdoll_tar/
lib.rs

1//! [Tar](https://en.wikipedia.org/wiki/Tar_%28computing%29) archive container format for [paperdoll](https://github.com/fralonra/paperdoll).
2//!
3//! # Examples
4//!
5//! ```no_run
6//! use paperdoll_tar::paperdoll;
7//!
8//! let factory = paperdoll::PaperdollFactory::default();
9//!
10//! paperdoll_tar::save(&mut factory.to_manifest(), "/path/to/save/your.ppd");
11//!
12//! let factory = paperdoll_tar::load("/path/to/save/your.ppd").unwrap();
13//! ```
14
15use std::{
16    fs::{write, File},
17    io::{read_to_string, Read},
18    path::{Path, PathBuf},
19};
20
21use anyhow::Result;
22pub use paperdoll;
23use paperdoll::{Manifest, PaperdollFactory};
24use tar::{Archive, Builder};
25use tempdir::TempDir;
26
27/// The file extension.
28pub const EXTENSION_NAME: &'static str = "ppd";
29
30/// The file name of the manifest file saved in the `ppd` file.
31pub const FILE_NAME_MANIFEST: &'static str = "manifest.yml";
32
33const FILE_NAME_TEMP_DIR: &'static str = "paperdoll_ppd_";
34
35/// Loads a paperdoll project from the path of a `ppd` file.
36pub fn load<P>(path: P) -> Result<PaperdollFactory>
37where
38    P: AsRef<Path>,
39{
40    read(File::open(&path)?)
41}
42
43/// Reads a paperdoll project from a reader containing the bytes of a `ppd` file.
44pub fn read<R>(r: R) -> Result<PaperdollFactory>
45where
46    R: Read,
47{
48    let mut archive = Archive::new(r);
49
50    let temp_dir = TempDir::new(FILE_NAME_TEMP_DIR)?;
51    archive.unpack(temp_dir.path())?;
52
53    let manifest_path = temp_dir.path().clone().join(FILE_NAME_MANIFEST);
54    let manifest_file = File::open(manifest_path)?;
55
56    let mut manifest: Manifest = serde_yaml::from_str(&read_to_string(manifest_file)?)?;
57
58    for doll in &mut manifest.dolls {
59        if doll.path.is_empty() {
60            continue;
61        }
62
63        let img_path = temp_dir.path().clone().join(&doll.path);
64        let img = image::open(img_path)?.into_rgba8();
65
66        doll.image.width = img.width();
67        doll.image.height = img.height();
68        doll.image.pixels = img.into_vec();
69    }
70
71    for fragment in &mut manifest.fragments {
72        if fragment.path.is_empty() {
73            continue;
74        }
75
76        let img_path = temp_dir.path().clone().join(&fragment.path);
77        let img = image::open(img_path)?.into_rgba8();
78
79        fragment.image.width = img.width();
80        fragment.image.height = img.height();
81        fragment.image.pixels = img.into_vec();
82    }
83
84    temp_dir.close()?;
85
86    PaperdollFactory::from_manifest(manifest)
87}
88
89/// Saves a `ppd` file using the given manifest to the path.
90pub fn save<P>(manifest: &mut Manifest, path: P) -> Result<()>
91where
92    P: AsRef<Path>,
93{
94    let temp_dir = TempDir::new(FILE_NAME_TEMP_DIR)?;
95
96    for doll in &mut manifest.dolls {
97        if doll.image.is_empty() {
98            continue;
99        }
100
101        let extension = (!doll.path.is_empty())
102            .then(|| {
103                PathBuf::from(&doll.path)
104                    .extension()
105                    .map(|ext| ext.to_string_lossy().to_string())
106            })
107            .flatten()
108            .map(|ext| {
109                // Saving WebP is not supported
110                if &ext == "webp" {
111                    "png".to_owned()
112                } else {
113                    ext
114                }
115            })
116            .unwrap_or("png".to_owned());
117
118        let filename = format!("doll_{}.{}", doll.id(), extension);
119
120        let img_path = temp_dir.path().clone().join(&filename);
121
122        doll.path = filename;
123
124        image::save_buffer(
125            img_path,
126            &doll.image.pixels,
127            doll.image.width,
128            doll.image.height,
129            image::ColorType::Rgba8,
130        )?;
131    }
132
133    for fragment in &mut manifest.fragments {
134        if fragment.image.is_empty() {
135            continue;
136        }
137
138        let extension = (!fragment.path.is_empty())
139            .then(|| {
140                PathBuf::from(&fragment.path)
141                    .extension()
142                    .map(|ext| ext.to_string_lossy().to_string())
143            })
144            .flatten()
145            .map(|ext| {
146                // Saving WebP is not supported
147                if &ext == "webp" {
148                    "png".to_owned()
149                } else {
150                    ext
151                }
152            })
153            .unwrap_or("png".to_owned());
154
155        let filename = format!("fragment_{}.{}", fragment.id(), extension);
156
157        let img_path = temp_dir.path().clone().join(&filename);
158
159        fragment.path = filename;
160
161        image::save_buffer(
162            img_path,
163            &fragment.image.pixels,
164            fragment.image.width,
165            fragment.image.height,
166            image::ColorType::Rgba8,
167        )?;
168    }
169
170    let manifest_str = serde_yaml::to_string(manifest)?;
171
172    let manifest_path = temp_dir.path().clone().join(FILE_NAME_MANIFEST);
173    write(manifest_path, manifest_str)?;
174
175    let output = File::create(path)?;
176
177    let mut archive = Builder::new(output);
178
179    archive.append_dir_all(".", temp_dir.path())?;
180
181    archive.finish()?;
182
183    temp_dir.close()?;
184
185    Ok(())
186}