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;
25use std::num::NonZeroU32;
26
27#[derive(Debug, Clone, Copy)]
28pub struct PreviewOptions {
29 pub max_cell: u32,
31}
32
33#[derive(Debug, Clone)]
34pub struct PreviewSet {
35 pub set_name: Option<String>,
36 pub entries: Vec<PreviewEntry>,
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq)]
42pub struct SourceDimensions {
43 width: NonZeroU32,
44 height: NonZeroU32,
45}
46
47impl SourceDimensions {
48 pub fn new(width: u32, height: u32) -> Option<Self> {
49 Some(Self {
50 width: NonZeroU32::new(width)?,
51 height: NonZeroU32::new(height)?,
52 })
53 }
54
55 pub fn width(self) -> u32 {
56 self.width.get()
57 }
58 pub fn height(self) -> u32 {
59 self.height.get()
60 }
61}
62
63#[derive(Debug, Clone)]
64pub struct PreviewEntry {
65 pub index: usize,
66 pub name: String,
67 pub tip: TipPreview,
68 pub source_dimensions: Option<SourceDimensions>,
70}
71
72#[derive(Debug, Clone)]
73pub enum TipPreview {
74 Available(GrayscaleBitmap),
75 Unavailable(UnavailableReason),
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum UnavailableReason {
80 NoShapePng,
81 UnsupportedTipKind(String),
82 Corrupt(String),
83 TooLarge { width: u32, height: u32 },
84}
85
86#[derive(Debug)]
87pub struct PreviewError(pub String);
88
89impl std::fmt::Display for PreviewError {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.write_str(&self.0)
92 }
93}
94
95impl std::error::Error for PreviewError {}
96
97fn check_max_cell(opts: PreviewOptions) -> Result<u32, PreviewError> {
98 if opts.max_cell == 0 {
99 return Err(PreviewError("max_cell must be at least 1".to_string()));
100 }
101 Ok(opts.max_cell)
102}
103
104#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
107enum Source {
108 Sampled,
109 Computed,
110 Unsupported,
111}
112
113struct Row {
114 key: usize,
116 source: Source,
117 name: String,
118 tip: TipPreview,
119 source_dimensions: Option<SourceDimensions>,
120}
121
122pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
131 let max_cell = check_max_cell(opts)?;
132
133 let deferred =
134 parse_abr_deferred_without_patterns(bytes).map_err(|e| PreviewError(e.to_string()))?;
135
136 let mut rows: Vec<Row> = Vec::new();
137
138 for i in 0..deferred.pack.brushes.len() {
139 let brush = &deferred.pack.brushes[i];
140 let key = brush.preset_index.unwrap_or(usize::MAX);
141 let name = if brush.name.is_empty() {
142 brush.id.clone()
143 } else {
144 brush.name.clone()
145 };
146 let source_dimensions = SourceDimensions::new(brush.tip.width, brush.tip.height);
147 let tip = match deferred.decode_tip(i) {
148 Ok(tip) => TipPreview::Available(downsample(&to_grayscale(&tip), max_cell)),
149 Err(e) => TipPreview::Unavailable(UnavailableReason::Corrupt(e.to_string())),
150 };
151 rows.push(Row {
152 key,
153 source: Source::Sampled,
154 source_dimensions,
155 name,
156 tip,
157 });
158 }
159
160 for preset in &deferred.pack.computed_presets {
161 let key = preset.preset_index.unwrap_or(usize::MAX);
162 let tip = match preset
163 .descriptor
164 .computed
165 .as_ref()
166 .filter(|geom| can_synthesize(geom))
167 .and_then(synthesize_computed_tip)
168 {
169 Some(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
170 None => TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(
171 "computed".to_string(),
172 )),
173 };
174 rows.push(Row {
175 key,
176 source: Source::Computed,
177 source_dimensions: None,
178 name: preset.name.clone(),
179 tip,
180 });
181 }
182
183 for preset in &deferred.pack.unsupported_tip_presets {
184 let kind = match (&preset.tip_shape, &preset.shape_tip_family) {
185 (Some(shape), _) => format!("{shape:?}"),
186 (None, Some(ShapeTipFamily::Bristle)) => "bristle".to_string(),
187 (None, Some(ShapeTipFamily::Erodible)) => "erodible".to_string(),
188 (None, None) => "shape tip".to_string(),
189 };
190 rows.push(Row {
191 key: preset.preset_index,
192 source: Source::Unsupported,
193 source_dimensions: None,
194 name: preset.name.clone(),
195 tip: TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
196 });
197 }
198
199 rows.sort_by_key(|row| (row.key, row.source));
200
201 let entries = rows
202 .into_iter()
203 .enumerate()
204 .map(|(index, row)| PreviewEntry {
205 index,
206 name: row.name,
207 tip: row.tip,
208 source_dimensions: row.source_dimensions,
209 })
210 .collect();
211
212 Ok(PreviewSet {
213 set_name: None,
214 entries,
215 })
216}
217
218pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
224 let max_cell = check_max_cell(opts)?;
225 let mut zip = open_zip(bytes)?;
226
227 let (set_name, prefixes) = if zip.by_name("brushset.plist").is_ok() {
228 let buf = procreate::read_zip_entry(&mut zip, "brushset.plist").map_err(PreviewError)?;
229 let (name, uuids) = procreate::parse_brushset_plist(&buf).map_err(PreviewError)?;
230 (name, uuids.into_iter().map(|u| format!("{u}/")).collect())
231 } else {
232 let members: Vec<String> = procreate::members_in_zip_order(&mut zip)
233 .into_iter()
234 .map(|d| format!("{d}/"))
235 .collect();
236 if members.is_empty() {
237 return Err(PreviewError("no brushes found".to_string()));
238 }
239 (None, members)
240 };
241
242 let entries = prefixes
243 .iter()
244 .enumerate()
245 .map(|(index, prefix)| member_entry(&mut zip, index, prefix, max_cell))
246 .collect();
247
248 Ok(PreviewSet { set_name, entries })
249}
250
251pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
254 let max_cell = check_max_cell(opts)?;
255 let mut zip = open_zip(bytes)?;
256
257 if zip.by_name("Brush.archive").is_err() {
258 return Err(PreviewError("Brush.archive not found".to_string()));
259 }
260
261 Ok(PreviewSet {
262 set_name: None,
263 entries: vec![member_entry(&mut zip, 0, "", max_cell)],
264 })
265}
266
267fn open_zip(bytes: &[u8]) -> Result<zip::ZipArchive<Cursor<&[u8]>>, PreviewError> {
268 zip::ZipArchive::new(Cursor::new(bytes))
269 .map_err(|e| PreviewError(format!("failed to open zip: {e}")))
270}
271
272fn member_entry(
279 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
280 index: usize,
281 prefix: &str,
282 max_cell: u32,
283) -> PreviewEntry {
284 let fallback_name = if prefix.is_empty() {
285 "Brush".to_string()
286 } else {
287 prefix.trim_end_matches('/').to_string()
288 };
289
290 let archive = procreate::read_zip_entry(zip, &format!("{prefix}Brush.archive"))
291 .and_then(|buf| procreate::brush_name(&buf));
292 let name = match archive {
293 Ok(name) => name.unwrap_or(fallback_name),
294 Err(msg) => {
295 return PreviewEntry {
296 index,
297 name: fallback_name,
298 source_dimensions: None,
299 tip: TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
300 }
301 }
302 };
303
304 let shape_path = format!("{prefix}Shape.png");
305 if zip.by_name(&shape_path).is_err() {
306 return PreviewEntry {
307 index,
308 name,
309 tip: TipPreview::Unavailable(UnavailableReason::NoShapePng),
310 source_dimensions: None,
311 };
312 }
313
314 let mut source_dimensions = None;
315 let tip = match procreate::read_zip_entry(zip, &shape_path) {
316 Err(msg) => TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
317 Ok(png) => {
318 source_dimensions = procreate::header_dimensions(&png)
319 .and_then(|(width, height)| SourceDimensions::new(width, height));
320 match procreate::decode_tip_png(&png) {
321 Ok(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
322 Err(procreate::ShapePngError::TooLarge { width, height }) => {
323 TipPreview::Unavailable(UnavailableReason::TooLarge { width, height })
324 }
325 Err(procreate::ShapePngError::Corrupt(msg)) => {
326 TipPreview::Unavailable(UnavailableReason::Corrupt(msg))
327 }
328 }
329 }
330 };
331
332 PreviewEntry {
333 index,
334 name,
335 tip,
336 source_dimensions,
337 }
338}