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_all_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),
79 Unavailable(UnavailableReason),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum UnavailableReason {
84 NoShapePng,
85 UnsupportedTipKind(String),
86 Corrupt(String),
87 TooLarge {
91 width: u32,
92 height: u32,
93 },
94}
95
96#[derive(Debug)]
97pub struct PreviewError(pub String);
98
99impl std::fmt::Display for PreviewError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.write_str(&self.0)
102 }
103}
104
105impl std::error::Error for PreviewError {}
106
107fn check_max_cell(opts: PreviewOptions) -> Result<u32, PreviewError> {
108 if opts.max_cell == 0 {
109 return Err(PreviewError("max_cell must be at least 1".to_string()));
110 }
111 Ok(opts.max_cell)
112}
113
114#[derive(Clone, Copy)]
116enum Take {
117 All,
118 FirstAvailable(usize),
121}
122
123impl Take {
124 fn collect(self, entries: impl Iterator<Item = PreviewEntry>) -> Vec<PreviewEntry> {
125 match self {
126 Take::All => entries.collect(),
127 Take::FirstAvailable(n) => entries
128 .filter(|entry| matches!(entry.tip, TipPreview::Available(_)))
129 .take(n)
130 .collect(),
131 }
132 }
133}
134
135#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
141enum Source {
142 Sampled(usize),
143 Computed(usize),
144 Unsupported(usize),
145}
146
147struct Row {
148 key: usize,
150 source: Source,
151}
152
153pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
165 abr(bytes, opts, Take::All)
166}
167
168pub fn preview_abr_first_available(
175 bytes: &[u8],
176 opts: PreviewOptions,
177 n: usize,
178) -> Result<PreviewSet, PreviewError> {
179 abr(bytes, opts, Take::FirstAvailable(n))
180}
181
182fn abr(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
183 let max_cell = check_max_cell(opts)?;
184
185 let deferred =
186 parse_abr_all_deferred_without_patterns(bytes).map_err(|e| PreviewError(e.to_string()))?;
187 let pack = &deferred.pack;
188
189 let mut rows: Vec<Row> = Vec::new();
190 rows.extend(pack.brushes.iter().enumerate().map(|(i, brush)| Row {
191 key: brush.preset_index.unwrap_or(usize::MAX),
192 source: Source::Sampled(i),
193 }));
194 rows.extend(
195 pack.computed_presets
196 .iter()
197 .enumerate()
198 .map(|(i, preset)| Row {
199 key: preset.preset_index.unwrap_or(usize::MAX),
200 source: Source::Computed(i),
201 }),
202 );
203 rows.extend(
204 pack.unsupported_tip_presets
205 .iter()
206 .enumerate()
207 .map(|(i, preset)| Row {
208 key: preset.preset_index,
209 source: Source::Unsupported(i),
210 }),
211 );
212
213 rows.sort_by_key(|row| (row.key, row.source));
214
215 let entries = rows
216 .into_iter()
217 .enumerate()
218 .map(|(index, row)| abr_entry(&deferred, index, row.source, max_cell));
219
220 Ok(PreviewSet {
221 set_name: None,
222 entries: take.collect(entries),
223 })
224}
225
226fn abr_entry(
229 deferred: &DeferredPack<'_>,
230 index: usize,
231 source: Source,
232 max_cell: u32,
233) -> PreviewEntry {
234 #[cfg(test)]
235 tests::record_entry();
236 let pack = &deferred.pack;
237 let (name, tip, source_dimensions) = match source {
238 Source::Sampled(i) => {
239 let brush = &pack.brushes[i];
240 let name = if brush.name.is_empty() {
241 brush.id.clone()
242 } else {
243 brush.name.clone()
244 };
245 let tip = match deferred.decode_tip(i) {
246 Ok(tip) => tip_preview(&to_grayscale(&tip), max_cell),
247 Err(e) => TipPreview::Unavailable(UnavailableReason::Corrupt(e.to_string())),
248 };
249 (
250 name,
251 tip,
252 SourceDimensions::new(brush.tip.width, brush.tip.height),
253 )
254 }
255 Source::Computed(i) => {
256 let preset = &pack.computed_presets[i];
257 let tip = match preset
258 .descriptor
259 .computed
260 .as_ref()
261 .filter(|geom| can_synthesize(geom))
262 .and_then(synthesize_computed_tip)
263 {
264 Some(bitmap) => tip_preview(&bitmap, max_cell),
265 None => TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(
266 "computed".to_string(),
267 )),
268 };
269 (preset.name.clone(), tip, None)
270 }
271 Source::Unsupported(i) => {
272 let preset = &pack.unsupported_tip_presets[i];
273 let kind = match (&preset.tip_shape, &preset.shape_tip_family) {
274 (Some(shape), _) => format!("{shape:?}"),
275 (None, Some(ShapeTipFamily::Bristle)) => "bristle".to_string(),
276 (None, Some(ShapeTipFamily::Erodible)) => "erodible".to_string(),
277 (None, None) => "shape tip".to_string(),
278 };
279 (
280 preset.name.clone(),
281 TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
282 None,
283 )
284 }
285 };
286 PreviewEntry {
287 index,
288 name,
289 tip,
290 source_dimensions,
291 }
292}
293
294fn tip_preview(bitmap: &GrayscaleBitmap, max_cell: u32) -> TipPreview {
298 if bitmap.width == 0 || bitmap.height == 0 {
299 return TipPreview::Unavailable(UnavailableReason::Corrupt(
300 "tip has zero area".to_string(),
301 ));
302 }
303 TipPreview::Available(downsample(bitmap, max_cell))
304}
305
306pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
312 brushset(bytes, opts, Take::All)
313}
314
315pub fn preview_brushset_first_available(
320 bytes: &[u8],
321 opts: PreviewOptions,
322 n: usize,
323) -> Result<PreviewSet, PreviewError> {
324 brushset(bytes, opts, Take::FirstAvailable(n))
325}
326
327fn brushset(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
328 let max_cell = check_max_cell(opts)?;
329 let mut zip = open_zip(bytes)?;
330
331 let (set_name, prefixes) = if zip.by_name("brushset.plist").is_ok() {
332 let buf = procreate::read_zip_entry(&mut zip, "brushset.plist").map_err(PreviewError)?;
333 let (name, uuids) = procreate::parse_brushset_plist(&buf).map_err(PreviewError)?;
334 (name, uuids.into_iter().map(|u| format!("{u}/")).collect())
335 } else {
336 let members: Vec<String> = procreate::members_in_zip_order(&mut zip)
337 .into_iter()
338 .map(|d| format!("{d}/"))
339 .collect();
340 if members.is_empty() {
341 return Err(PreviewError("no brushes found".to_string()));
342 }
343 (None, members)
344 };
345
346 let entries = prefixes
347 .iter()
348 .enumerate()
349 .map(|(index, prefix)| member_entry(&mut zip, index, prefix, max_cell));
350
351 Ok(PreviewSet {
352 set_name,
353 entries: take.collect(entries),
354 })
355}
356
357pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
360 brush(bytes, opts, Take::All)
361}
362
363pub fn preview_brush_first_available(
367 bytes: &[u8],
368 opts: PreviewOptions,
369 n: usize,
370) -> Result<PreviewSet, PreviewError> {
371 brush(bytes, opts, Take::FirstAvailable(n))
372}
373
374fn brush(bytes: &[u8], opts: PreviewOptions, take: Take) -> Result<PreviewSet, PreviewError> {
375 let max_cell = check_max_cell(opts)?;
376 let mut zip = open_zip(bytes)?;
377
378 if zip.by_name("Brush.archive").is_err() {
379 return Err(PreviewError("Brush.archive not found".to_string()));
380 }
381
382 let entry = std::iter::once_with(|| member_entry(&mut zip, 0, "", max_cell));
383 Ok(PreviewSet {
384 set_name: None,
385 entries: take.collect(entry),
386 })
387}
388
389fn open_zip(bytes: &[u8]) -> Result<zip::ZipArchive<Cursor<&[u8]>>, PreviewError> {
390 zip::ZipArchive::new(Cursor::new(bytes))
391 .map_err(|e| PreviewError(format!("failed to open zip: {e}")))
392}
393
394fn member_entry(
401 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
402 index: usize,
403 prefix: &str,
404 max_cell: u32,
405) -> PreviewEntry {
406 #[cfg(test)]
407 tests::record_entry();
408 let fallback_name = if prefix.is_empty() {
409 "Brush".to_string()
410 } else {
411 prefix.trim_end_matches('/').to_string()
412 };
413
414 let archive = procreate::read_zip_entry(zip, &format!("{prefix}Brush.archive"))
415 .and_then(|buf| procreate::brush_name(&buf));
416
417 let shape_path = format!("{prefix}Shape.png");
418 let shape = if zip.by_name(&shape_path).is_ok() {
419 Some(procreate::read_zip_entry(zip, &shape_path))
420 } else {
421 None
422 };
423 let source_dimensions = match &shape {
424 Some(Ok(png)) => bitmap::header_dimensions(png)
425 .and_then(|(width, height)| SourceDimensions::new(width, height)),
426 _ => None,
427 };
428
429 let (name, tip) = match archive {
430 Ok(name) => (name.unwrap_or(fallback_name), shape_tip(shape, max_cell)),
431 Err(msg) => (
432 fallback_name,
433 TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
434 ),
435 };
436
437 PreviewEntry {
438 index,
439 name,
440 tip,
441 source_dimensions,
442 }
443}
444
445fn shape_tip(shape: Option<Result<Vec<u8>, String>>, max_cell: u32) -> TipPreview {
448 let png = match shape {
449 None => return TipPreview::Unavailable(UnavailableReason::NoShapePng),
450 Some(Err(msg)) => return TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
451 Some(Ok(png)) => png,
452 };
453 match procreate::decode_tip_png(&png) {
454 Ok(bitmap) => tip_preview(&bitmap, max_cell),
455 Err(procreate::ShapePngError::TooLarge { width, height }) => {
456 TipPreview::Unavailable(UnavailableReason::TooLarge { width, height })
457 }
458 Err(procreate::ShapePngError::Corrupt(msg)) => {
459 TipPreview::Unavailable(UnavailableReason::Corrupt(msg))
460 }
461 }
462}
463
464#[cfg(test)]
466#[path = "../tests/common/mod.rs"]
467mod common;
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::common::{
473 brush_archive, brushset_plist, gray_png, legacy_abr, samp_abr, zip_with, SampTip,
474 };
475 use std::cell::Cell;
476
477 thread_local! {
478 static ENTRIES: Cell<usize> = const { Cell::new(0) };
480 }
481
482 pub(super) fn record_entry() {
484 ENTRIES.with(|count| count.set(count.get() + 1));
485 }
486
487 fn entries_built<T>(f: impl FnOnce() -> T) -> usize {
489 ENTRIES.with(|count| count.set(0));
490 f();
491 ENTRIES.with(Cell::get)
492 }
493
494 const OPTS: PreviewOptions = PreviewOptions { max_cell: 8 };
495
496 fn tip(corrupt: bool) -> SampTip {
497 SampTip {
498 width: 4,
499 height: 4,
500 fill: 0x80,
501 corrupt,
502 }
503 }
504
505 #[test]
506 fn abr_builds_only_the_entries_it_returns() {
507 let bytes = samp_abr(&[
508 tip(false),
509 tip(false),
510 tip(false),
511 tip(false),
512 tip(false),
513 tip(false),
514 ]);
515 assert_eq!(
516 entries_built(|| preview_abr_first_available(&bytes, OPTS, 2).unwrap()),
517 2
518 );
519 assert_eq!(
520 entries_built(|| preview_abr_first_available(&bytes, OPTS, 0).unwrap()),
521 0
522 );
523 assert_eq!(entries_built(|| preview_abr(&bytes, OPTS).unwrap()), 6);
524 }
525
526 #[test]
527 fn abr_failed_decode_does_not_count_toward_n() {
528 let bytes = samp_abr(&[tip(false), tip(false), tip(true)]);
529 assert!(matches!(
530 preview_abr(&bytes, OPTS).unwrap().entries[0].tip,
531 TipPreview::Unavailable(UnavailableReason::Corrupt(_))
532 ));
533 let mut set = None;
534 assert_eq!(
535 entries_built(|| set = Some(preview_abr_first_available(&bytes, OPTS, 1).unwrap())),
536 2
537 );
538 let entries = set.unwrap().entries;
539 assert_eq!(entries.len(), 1);
540 assert_eq!(entries[0].index, 1);
541 }
542
543 #[test]
544 fn abr_v2_corrupt_tip_is_one_corrupt_entry() {
545 let bytes = legacy_abr(&[tip(false), tip(true), tip(false)]);
546 let entries = preview_abr(&bytes, OPTS).unwrap().entries;
547 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
548 assert_eq!(names, ["brush_2", "brush_1", "brush_0"]);
549 assert!(matches!(entries[0].tip, TipPreview::Available(_)));
550 assert!(matches!(
551 entries[1].tip,
552 TipPreview::Unavailable(UnavailableReason::Corrupt(_))
553 ));
554 assert!(matches!(entries[2].tip, TipPreview::Available(_)));
555 }
556
557 #[test]
558 fn abr_zero_area_tip_is_unavailable_and_does_not_count_toward_n() {
559 let sized = |width, height| SampTip {
560 width,
561 height,
562 fill: 0x80,
563 corrupt: false,
564 };
565 let bytes = legacy_abr(&[
569 sized(1, 1),
570 sized(0, 20),
571 sized(0, 1),
572 sized(0, 1),
573 sized(20, 0),
574 ]);
575
576 let entries = preview_abr(&bytes, OPTS).unwrap().entries;
577 assert_eq!(entries.len(), 5);
578 for entry in &entries[..4] {
579 assert!(
580 matches!(
581 &entry.tip,
582 TipPreview::Unavailable(UnavailableReason::Corrupt(msg)) if msg == "tip has zero area"
583 ),
584 "{entry:?}"
585 );
586 }
587
588 let entries = preview_abr_first_available(&bytes, OPTS, 4)
589 .unwrap()
590 .entries;
591 assert_eq!(entries.len(), 1);
592 assert_eq!(entries[0].index, 4);
593 assert!(matches!(
594 &entries[0].tip,
595 TipPreview::Available(b) if (b.width, b.height, b.data.as_slice()) == (1, 1, &[0x80][..])
596 ));
597 }
598
599 #[test]
600 fn brushset_reads_only_the_members_it_returns() {
601 let archive = brush_archive("Tip");
602 let shape = gray_png(4, 4, 200);
603 let plist = brushset_plist("Set", &["a", "b", "c", "d"]);
604 let mut files: Vec<(String, &[u8])> = vec![("brushset.plist".into(), &plist)];
605 for member in ["a", "b", "c", "d"] {
606 files.push((format!("{member}/Brush.archive"), &archive));
607 files.push((format!("{member}/Shape.png"), &shape));
608 }
609 let files: Vec<(&str, &[u8])> = files.iter().map(|(p, b)| (p.as_str(), *b)).collect();
610 let bytes = zip_with(&files);
611 assert_eq!(
612 entries_built(|| preview_brushset_first_available(&bytes, OPTS, 1).unwrap()),
613 1
614 );
615 assert_eq!(entries_built(|| preview_brushset(&bytes, OPTS).unwrap()), 4);
616 }
617}