1pub mod procreate;
14
15mod bitmap;
16#[cfg(feature = "text")]
17mod sheet;
18mod synth;
19
20pub use bitmap::*;
21#[cfg(feature = "text")]
22pub use sheet::*;
23pub use synth::*;
24
25use brushkit_abr::{parse_abr_deferred_without_patterns, DeferredPack, ShapeTipFamily};
26use std::io::Cursor;
27use std::num::NonZeroU32;
28
29#[derive(Debug, Clone, Copy)]
30pub struct PreviewOptions {
31 pub max_cell: u32,
33}
34
35#[derive(Debug, Clone)]
36pub struct PreviewSet {
37 pub set_name: Option<String>,
38 pub entries: Vec<PreviewEntry>,
39}
40
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
44pub struct SourceDimensions {
45 width: NonZeroU32,
46 height: NonZeroU32,
47}
48
49impl SourceDimensions {
50 pub fn new(width: u32, height: u32) -> Option<Self> {
51 Some(Self {
52 width: NonZeroU32::new(width)?,
53 height: NonZeroU32::new(height)?,
54 })
55 }
56
57 pub fn width(self) -> u32 {
58 self.width.get()
59 }
60 pub fn height(self) -> u32 {
61 self.height.get()
62 }
63}
64
65#[derive(Debug, Clone)]
66pub struct PreviewEntry {
67 pub index: usize,
68 pub name: String,
69 pub tip: TipPreview,
70 pub source_dimensions: Option<SourceDimensions>,
72}
73
74#[derive(Debug, Clone)]
75pub enum TipPreview {
76 Available(GrayscaleBitmap),
77 Unavailable(UnavailableReason),
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum UnavailableReason {
82 NoShapePng,
83 UnsupportedTipKind(String),
84 Corrupt(String),
85 TooLarge { width: u32, height: u32 },
86}
87
88#[derive(Debug)]
89pub struct PreviewError(pub String);
90
91impl std::fmt::Display for PreviewError {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.write_str(&self.0)
94 }
95}
96
97impl std::error::Error for PreviewError {}
98
99fn check_max_cell(opts: PreviewOptions) -> Result<u32, PreviewError> {
100 if opts.max_cell == 0 {
101 return Err(PreviewError("max_cell must be at least 1".to_string()));
102 }
103 Ok(opts.max_cell)
104}
105
106#[derive(Clone, Copy)]
108enum Take {
109 All,
110 FirstAvailable(usize),
113}
114
115impl Take {
116 fn collect(self, entries: impl Iterator<Item = PreviewEntry>) -> Vec<PreviewEntry> {
117 match self {
118 Take::All => entries.collect(),
119 Take::FirstAvailable(n) => entries
120 .filter(|entry| matches!(entry.tip, TipPreview::Available(_)))
121 .take(n)
122 .collect(),
123 }
124 }
125}
126
127#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
133enum Source {
134 Sampled(usize),
135 Computed(usize),
136 Unsupported(usize),
137}
138
139struct Row {
140 key: usize,
142 source: Source,
143}
144
145pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
154 abr(bytes, opts, Take::All)
155}
156
157pub fn preview_abr_first_available(
166 bytes: &[u8],
167 opts: PreviewOptions,
168 n: usize,
169) -> Result<PreviewSet, PreviewError> {
170 abr(bytes, opts, Take::FirstAvailable(n))
171}
172
173fn abr(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
174 let max_cell = check_max_cell(opts)?;
175
176 let deferred =
177 parse_abr_deferred_without_patterns(bytes).map_err(|e| PreviewError(e.to_string()))?;
178 let pack = &deferred.pack;
179
180 let mut rows: Vec<Row> = Vec::new();
181 rows.extend(pack.brushes.iter().enumerate().map(|(i, brush)| Row {
182 key: brush.preset_index.unwrap_or(usize::MAX),
183 source: Source::Sampled(i),
184 }));
185 rows.extend(
186 pack.computed_presets
187 .iter()
188 .enumerate()
189 .map(|(i, preset)| Row {
190 key: preset.preset_index.unwrap_or(usize::MAX),
191 source: Source::Computed(i),
192 }),
193 );
194 rows.extend(
195 pack.unsupported_tip_presets
196 .iter()
197 .enumerate()
198 .map(|(i, preset)| Row {
199 key: preset.preset_index,
200 source: Source::Unsupported(i),
201 }),
202 );
203
204 rows.sort_by_key(|row| (row.key, row.source));
205
206 let entries = rows
207 .into_iter()
208 .enumerate()
209 .map(|(index, row)| abr_entry(&deferred, index, row.source, max_cell));
210
211 Ok(PreviewSet {
212 set_name: None,
213 entries: take.collect(entries),
214 })
215}
216
217fn abr_entry(deferred: &DeferredPack, index: usize, source: Source, max_cell: u32) -> PreviewEntry {
220 #[cfg(test)]
221 tests::record_entry();
222 let pack = &deferred.pack;
223 let (name, tip, source_dimensions) = match source {
224 Source::Sampled(i) => {
225 let brush = &pack.brushes[i];
226 let name = if brush.name.is_empty() {
227 brush.id.clone()
228 } else {
229 brush.name.clone()
230 };
231 let tip = match deferred.decode_tip(i) {
232 Ok(tip) => TipPreview::Available(downsample(&to_grayscale(&tip), max_cell)),
233 Err(e) => TipPreview::Unavailable(UnavailableReason::Corrupt(e.to_string())),
234 };
235 (
236 name,
237 tip,
238 SourceDimensions::new(brush.tip.width, brush.tip.height),
239 )
240 }
241 Source::Computed(i) => {
242 let preset = &pack.computed_presets[i];
243 let tip = match preset
244 .descriptor
245 .computed
246 .as_ref()
247 .filter(|geom| can_synthesize(geom))
248 .and_then(synthesize_computed_tip)
249 {
250 Some(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
251 None => TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(
252 "computed".to_string(),
253 )),
254 };
255 (preset.name.clone(), tip, None)
256 }
257 Source::Unsupported(i) => {
258 let preset = &pack.unsupported_tip_presets[i];
259 let kind = match (&preset.tip_shape, &preset.shape_tip_family) {
260 (Some(shape), _) => format!("{shape:?}"),
261 (None, Some(ShapeTipFamily::Bristle)) => "bristle".to_string(),
262 (None, Some(ShapeTipFamily::Erodible)) => "erodible".to_string(),
263 (None, None) => "shape tip".to_string(),
264 };
265 (
266 preset.name.clone(),
267 TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
268 None,
269 )
270 }
271 };
272 PreviewEntry {
273 index,
274 name,
275 tip,
276 source_dimensions,
277 }
278}
279
280pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
286 brushset(bytes, opts, Take::All)
287}
288
289pub fn preview_brushset_first_available(
294 bytes: &[u8],
295 opts: PreviewOptions,
296 n: usize,
297) -> Result<PreviewSet, PreviewError> {
298 brushset(bytes, opts, Take::FirstAvailable(n))
299}
300
301fn brushset(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
302 let max_cell = check_max_cell(opts)?;
303 let mut zip = open_zip(bytes)?;
304
305 let (set_name, prefixes) = if zip.by_name("brushset.plist").is_ok() {
306 let buf = procreate::read_zip_entry(&mut zip, "brushset.plist").map_err(PreviewError)?;
307 let (name, uuids) = procreate::parse_brushset_plist(&buf).map_err(PreviewError)?;
308 (name, uuids.into_iter().map(|u| format!("{u}/")).collect())
309 } else {
310 let members: Vec<String> = procreate::members_in_zip_order(&mut zip)
311 .into_iter()
312 .map(|d| format!("{d}/"))
313 .collect();
314 if members.is_empty() {
315 return Err(PreviewError("no brushes found".to_string()));
316 }
317 (None, members)
318 };
319
320 let entries = prefixes
321 .iter()
322 .enumerate()
323 .map(|(index, prefix)| member_entry(&mut zip, index, prefix, max_cell));
324
325 Ok(PreviewSet {
326 set_name,
327 entries: take.collect(entries),
328 })
329}
330
331pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
334 brush(bytes, opts, Take::All)
335}
336
337pub fn preview_brush_first_available(
341 bytes: &[u8],
342 opts: PreviewOptions,
343 n: usize,
344) -> Result<PreviewSet, PreviewError> {
345 brush(bytes, opts, Take::FirstAvailable(n))
346}
347
348fn brush(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
349 let max_cell = check_max_cell(opts)?;
350 let mut zip = open_zip(bytes)?;
351
352 if zip.by_name("Brush.archive").is_err() {
353 return Err(PreviewError("Brush.archive not found".to_string()));
354 }
355
356 let entry = std::iter::once_with(|| member_entry(&mut zip, 0, "", max_cell));
357 Ok(PreviewSet {
358 set_name: None,
359 entries: take.collect(entry),
360 })
361}
362
363fn open_zip(bytes: &[u8]) -> Result<zip::ZipArchive<Cursor<&[u8]>>, PreviewError> {
364 zip::ZipArchive::new(Cursor::new(bytes))
365 .map_err(|e| PreviewError(format!("failed to open zip: {e}")))
366}
367
368fn member_entry(
375 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
376 index: usize,
377 prefix: &str,
378 max_cell: u32,
379) -> PreviewEntry {
380 #[cfg(test)]
381 tests::record_entry();
382 let fallback_name = if prefix.is_empty() {
383 "Brush".to_string()
384 } else {
385 prefix.trim_end_matches('/').to_string()
386 };
387
388 let archive = procreate::read_zip_entry(zip, &format!("{prefix}Brush.archive"))
389 .and_then(|buf| procreate::brush_name(&buf));
390
391 let shape_path = format!("{prefix}Shape.png");
392 let shape = if zip.by_name(&shape_path).is_ok() {
393 Some(procreate::read_zip_entry(zip, &shape_path))
394 } else {
395 None
396 };
397 let source_dimensions = match &shape {
398 Some(Ok(png)) => procreate::header_dimensions(png)
399 .and_then(|(width, height)| SourceDimensions::new(width, height)),
400 _ => None,
401 };
402
403 let (name, tip) = match archive {
404 Ok(name) => (name.unwrap_or(fallback_name), shape_tip(shape, max_cell)),
405 Err(msg) => (
406 fallback_name,
407 TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
408 ),
409 };
410
411 PreviewEntry {
412 index,
413 name,
414 tip,
415 source_dimensions,
416 }
417}
418
419fn shape_tip(shape: Option<Result<Vec<u8>, String>>, max_cell: u32) -> TipPreview {
422 let png = match shape {
423 None => return TipPreview::Unavailable(UnavailableReason::NoShapePng),
424 Some(Err(msg)) => return TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
425 Some(Ok(png)) => png,
426 };
427 match procreate::decode_tip_png(&png) {
428 Ok(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
429 Err(procreate::ShapePngError::TooLarge { width, height }) => {
430 TipPreview::Unavailable(UnavailableReason::TooLarge { width, height })
431 }
432 Err(procreate::ShapePngError::Corrupt(msg)) => {
433 TipPreview::Unavailable(UnavailableReason::Corrupt(msg))
434 }
435 }
436}
437
438#[cfg(test)]
440#[path = "../tests/common/mod.rs"]
441mod common;
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::common::{brush_archive, brushset_plist, gray_png, samp_abr, zip_with, SampTip};
447 use std::cell::Cell;
448
449 thread_local! {
450 static ENTRIES: Cell<usize> = const { Cell::new(0) };
452 }
453
454 pub(super) fn record_entry() {
456 ENTRIES.with(|count| count.set(count.get() + 1));
457 }
458
459 fn entries_built<T>(f: impl FnOnce() -> T) -> usize {
461 ENTRIES.with(|count| count.set(0));
462 f();
463 ENTRIES.with(Cell::get)
464 }
465
466 const OPTS: PreviewOptions = PreviewOptions { max_cell: 8 };
467
468 fn tip(corrupt: bool) -> SampTip {
469 SampTip {
470 width: 4,
471 height: 4,
472 fill: 0x80,
473 corrupt,
474 }
475 }
476
477 #[test]
478 fn abr_builds_only_the_entries_it_returns() {
479 let bytes = samp_abr(&[
480 tip(false),
481 tip(false),
482 tip(false),
483 tip(false),
484 tip(false),
485 tip(false),
486 ]);
487 assert_eq!(
488 entries_built(|| preview_abr_first_available(&bytes, OPTS, 2).unwrap()),
489 2
490 );
491 assert_eq!(
492 entries_built(|| preview_abr_first_available(&bytes, OPTS, 0).unwrap()),
493 0
494 );
495 assert_eq!(entries_built(|| preview_abr(&bytes, OPTS).unwrap()), 6);
496 }
497
498 #[test]
499 fn abr_failed_decode_does_not_count_toward_n() {
500 let bytes = samp_abr(&[tip(false), tip(false), tip(true)]);
501 assert!(matches!(
502 preview_abr(&bytes, OPTS).unwrap().entries[0].tip,
503 TipPreview::Unavailable(UnavailableReason::Corrupt(_))
504 ));
505 let mut set = None;
506 assert_eq!(
507 entries_built(|| set = Some(preview_abr_first_available(&bytes, OPTS, 1).unwrap())),
508 2
509 );
510 let entries = set.unwrap().entries;
511 assert_eq!(entries.len(), 1);
512 assert_eq!(entries[0].index, 1);
513 }
514
515 #[test]
516 fn brushset_reads_only_the_members_it_returns() {
517 let archive = brush_archive("Tip");
518 let shape = gray_png(4, 4, 200);
519 let plist = brushset_plist("Set", &["a", "b", "c", "d"]);
520 let mut files: Vec<(String, &[u8])> = vec![("brushset.plist".into(), &plist)];
521 for member in ["a", "b", "c", "d"] {
522 files.push((format!("{member}/Brush.archive"), &archive));
523 files.push((format!("{member}/Shape.png"), &shape));
524 }
525 let files: Vec<(&str, &[u8])> = files.iter().map(|(p, b)| (p.as_str(), *b)).collect();
526 let bytes = zip_with(&files);
527 assert_eq!(
528 entries_built(|| preview_brushset_first_available(&bytes, OPTS, 1).unwrap()),
529 1
530 );
531 assert_eq!(entries_built(|| preview_brushset(&bytes, OPTS).unwrap()), 4);
532 }
533}