1pub mod procreate;
12
13mod bitmap;
14#[cfg(feature = "text")]
15mod sheet;
16mod synth;
17
18pub use bitmap::*;
19#[cfg(feature = "text")]
20pub use sheet::*;
21pub use synth::*;
22
23use brushkit_abr::{parse_abr_deferred_without_patterns, ShapeTipFamily};
24use std::io::Cursor;
25
26#[derive(Debug, Clone, Copy)]
27pub struct PreviewOptions {
28 pub max_cell: u32,
30}
31
32#[derive(Debug, Clone)]
33pub struct PreviewSet {
34 pub set_name: Option<String>,
35 pub entries: Vec<PreviewEntry>,
36}
37
38#[derive(Debug, Clone)]
39pub struct PreviewEntry {
40 pub index: usize,
41 pub name: String,
42 pub tip: TipPreview,
43}
44
45#[derive(Debug, Clone)]
46pub enum TipPreview {
47 Available(GrayscaleBitmap),
48 Unavailable(UnavailableReason),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum UnavailableReason {
53 NoShapePng,
54 UnsupportedTipKind(String),
55 Corrupt(String),
56 TooLarge { width: u32, height: u32 },
57}
58
59#[derive(Debug)]
60pub struct PreviewError(pub String);
61
62impl std::fmt::Display for PreviewError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.write_str(&self.0)
65 }
66}
67
68impl std::error::Error for PreviewError {}
69
70fn check_max_cell(opts: PreviewOptions) -> Result<u32, PreviewError> {
71 if opts.max_cell == 0 {
72 return Err(PreviewError("max_cell must be at least 1".to_string()));
73 }
74 Ok(opts.max_cell)
75}
76
77#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
80enum Source {
81 Sampled,
82 Computed,
83 Unsupported,
84}
85
86struct Row {
87 key: usize,
89 source: Source,
90 name: String,
91 tip: TipPreview,
92}
93
94pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
103 let max_cell = check_max_cell(opts)?;
104
105 let deferred =
106 parse_abr_deferred_without_patterns(bytes).map_err(|e| PreviewError(e.to_string()))?;
107
108 let mut rows: Vec<Row> = Vec::new();
109
110 for i in 0..deferred.pack.brushes.len() {
111 let brush = &deferred.pack.brushes[i];
112 let key = brush.preset_index.unwrap_or(usize::MAX);
113 let name = if brush.name.is_empty() {
114 brush.id.clone()
115 } else {
116 brush.name.clone()
117 };
118 let tip = match deferred.decode_tip(i) {
119 Ok(tip) => TipPreview::Available(downsample(&to_grayscale(&tip), max_cell)),
120 Err(e) => TipPreview::Unavailable(UnavailableReason::Corrupt(e.to_string())),
121 };
122 rows.push(Row {
123 key,
124 source: Source::Sampled,
125 name,
126 tip,
127 });
128 }
129
130 for preset in &deferred.pack.computed_presets {
131 let key = preset.preset_index.unwrap_or(usize::MAX);
132 let tip = match preset
133 .descriptor
134 .computed
135 .as_ref()
136 .filter(|geom| can_synthesize(geom))
137 .and_then(synthesize_computed_tip)
138 {
139 Some(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
140 None => TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(
141 "computed".to_string(),
142 )),
143 };
144 rows.push(Row {
145 key,
146 source: Source::Computed,
147 name: preset.name.clone(),
148 tip,
149 });
150 }
151
152 for preset in &deferred.pack.unsupported_tip_presets {
153 let kind = match (&preset.tip_shape, &preset.shape_tip_family) {
154 (Some(shape), _) => format!("{shape:?}"),
155 (None, Some(ShapeTipFamily::Bristle)) => "bristle".to_string(),
156 (None, Some(ShapeTipFamily::Erodible)) => "erodible".to_string(),
157 (None, None) => "shape tip".to_string(),
158 };
159 rows.push(Row {
160 key: preset.preset_index,
161 source: Source::Unsupported,
162 name: preset.name.clone(),
163 tip: TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
164 });
165 }
166
167 rows.sort_by_key(|row| (row.key, row.source));
168
169 let entries = rows
170 .into_iter()
171 .enumerate()
172 .map(|(index, row)| PreviewEntry {
173 index,
174 name: row.name,
175 tip: row.tip,
176 })
177 .collect();
178
179 Ok(PreviewSet {
180 set_name: None,
181 entries,
182 })
183}
184
185pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
191 let max_cell = check_max_cell(opts)?;
192 let mut zip = open_zip(bytes)?;
193
194 let (set_name, prefixes) = if zip.by_name("brushset.plist").is_ok() {
195 let buf = procreate::read_zip_entry(&mut zip, "brushset.plist").map_err(PreviewError)?;
196 let (name, uuids) = procreate::parse_brushset_plist(&buf).map_err(PreviewError)?;
197 (name, uuids.into_iter().map(|u| format!("{u}/")).collect())
198 } else {
199 let members: Vec<String> = procreate::members_in_zip_order(&mut zip)
200 .into_iter()
201 .map(|d| format!("{d}/"))
202 .collect();
203 if members.is_empty() {
204 return Err(PreviewError("no brushes found".to_string()));
205 }
206 (None, members)
207 };
208
209 let entries = prefixes
210 .iter()
211 .enumerate()
212 .map(|(index, prefix)| member_entry(&mut zip, index, prefix, max_cell))
213 .collect();
214
215 Ok(PreviewSet { set_name, entries })
216}
217
218pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
221 let max_cell = check_max_cell(opts)?;
222 let mut zip = open_zip(bytes)?;
223
224 if zip.by_name("Brush.archive").is_err() {
225 return Err(PreviewError("Brush.archive not found".to_string()));
226 }
227
228 Ok(PreviewSet {
229 set_name: None,
230 entries: vec![member_entry(&mut zip, 0, "", max_cell)],
231 })
232}
233
234fn open_zip(bytes: &[u8]) -> Result<zip::ZipArchive<Cursor<&[u8]>>, PreviewError> {
235 zip::ZipArchive::new(Cursor::new(bytes))
236 .map_err(|e| PreviewError(format!("failed to open zip: {e}")))
237}
238
239fn member_entry(
246 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
247 index: usize,
248 prefix: &str,
249 max_cell: u32,
250) -> PreviewEntry {
251 let fallback_name = if prefix.is_empty() {
252 "Brush".to_string()
253 } else {
254 prefix.trim_end_matches('/').to_string()
255 };
256
257 let archive = procreate::read_zip_entry(zip, &format!("{prefix}Brush.archive"))
258 .and_then(|buf| procreate::brush_name(&buf));
259 let name = match archive {
260 Ok(name) => name.unwrap_or(fallback_name),
261 Err(msg) => {
262 return PreviewEntry {
263 index,
264 name: fallback_name,
265 tip: TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
266 }
267 }
268 };
269
270 let shape_path = format!("{prefix}Shape.png");
271 if zip.by_name(&shape_path).is_err() {
272 return PreviewEntry {
273 index,
274 name,
275 tip: TipPreview::Unavailable(UnavailableReason::NoShapePng),
276 };
277 }
278
279 let tip = match procreate::read_zip_entry(zip, &shape_path) {
280 Err(msg) => TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
281 Ok(png) => match procreate::decode_tip_png(&png) {
282 Ok(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
283 Err(procreate::ShapePngError::TooLarge { width, height }) => {
284 TipPreview::Unavailable(UnavailableReason::TooLarge { width, height })
285 }
286 Err(procreate::ShapePngError::Corrupt(msg)) => {
287 TipPreview::Unavailable(UnavailableReason::Corrupt(msg))
288 }
289 },
290 };
291
292 PreviewEntry { index, name, tip }
293}