pub mod procreate;
mod bitmap;
#[cfg(feature = "text")]
mod sheet;
mod synth;
pub use bitmap::*;
#[cfg(feature = "text")]
pub use sheet::*;
pub use synth::*;
use brushkit_abr::{parse_abr_deferred_without_patterns, ShapeTipFamily};
use std::io::Cursor;
use std::num::NonZeroU32;
#[derive(Debug, Clone, Copy)]
pub struct PreviewOptions {
pub max_cell: u32,
}
#[derive(Debug, Clone)]
pub struct PreviewSet {
pub set_name: Option<String>,
pub entries: Vec<PreviewEntry>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct SourceDimensions {
width: NonZeroU32,
height: NonZeroU32,
}
impl SourceDimensions {
pub fn new(width: u32, height: u32) -> Option<Self> {
Some(Self {
width: NonZeroU32::new(width)?,
height: NonZeroU32::new(height)?,
})
}
pub fn width(self) -> u32 {
self.width.get()
}
pub fn height(self) -> u32 {
self.height.get()
}
}
#[derive(Debug, Clone)]
pub struct PreviewEntry {
pub index: usize,
pub name: String,
pub tip: TipPreview,
pub source_dimensions: Option<SourceDimensions>,
}
#[derive(Debug, Clone)]
pub enum TipPreview {
Available(GrayscaleBitmap),
Unavailable(UnavailableReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnavailableReason {
NoShapePng,
UnsupportedTipKind(String),
Corrupt(String),
TooLarge { width: u32, height: u32 },
}
#[derive(Debug)]
pub struct PreviewError(pub String);
impl std::fmt::Display for PreviewError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PreviewError {}
fn check_max_cell(opts: PreviewOptions) -> Result<u32, PreviewError> {
if opts.max_cell == 0 {
return Err(PreviewError("max_cell must be at least 1".to_string()));
}
Ok(opts.max_cell)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Source {
Sampled,
Computed,
Unsupported,
}
struct Row {
key: usize,
source: Source,
name: String,
tip: TipPreview,
source_dimensions: Option<SourceDimensions>,
}
pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
let max_cell = check_max_cell(opts)?;
let deferred =
parse_abr_deferred_without_patterns(bytes).map_err(|e| PreviewError(e.to_string()))?;
let mut rows: Vec<Row> = Vec::new();
for i in 0..deferred.pack.brushes.len() {
let brush = &deferred.pack.brushes[i];
let key = brush.preset_index.unwrap_or(usize::MAX);
let name = if brush.name.is_empty() {
brush.id.clone()
} else {
brush.name.clone()
};
let source_dimensions = SourceDimensions::new(brush.tip.width, brush.tip.height);
let tip = match deferred.decode_tip(i) {
Ok(tip) => TipPreview::Available(downsample(&to_grayscale(&tip), max_cell)),
Err(e) => TipPreview::Unavailable(UnavailableReason::Corrupt(e.to_string())),
};
rows.push(Row {
key,
source: Source::Sampled,
source_dimensions,
name,
tip,
});
}
for preset in &deferred.pack.computed_presets {
let key = preset.preset_index.unwrap_or(usize::MAX);
let tip = match preset
.descriptor
.computed
.as_ref()
.filter(|geom| can_synthesize(geom))
.and_then(synthesize_computed_tip)
{
Some(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
None => TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(
"computed".to_string(),
)),
};
rows.push(Row {
key,
source: Source::Computed,
source_dimensions: None,
name: preset.name.clone(),
tip,
});
}
for preset in &deferred.pack.unsupported_tip_presets {
let kind = match (&preset.tip_shape, &preset.shape_tip_family) {
(Some(shape), _) => format!("{shape:?}"),
(None, Some(ShapeTipFamily::Bristle)) => "bristle".to_string(),
(None, Some(ShapeTipFamily::Erodible)) => "erodible".to_string(),
(None, None) => "shape tip".to_string(),
};
rows.push(Row {
key: preset.preset_index,
source: Source::Unsupported,
source_dimensions: None,
name: preset.name.clone(),
tip: TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
});
}
rows.sort_by_key(|row| (row.key, row.source));
let entries = rows
.into_iter()
.enumerate()
.map(|(index, row)| PreviewEntry {
index,
name: row.name,
tip: row.tip,
source_dimensions: row.source_dimensions,
})
.collect();
Ok(PreviewSet {
set_name: None,
entries,
})
}
pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
let max_cell = check_max_cell(opts)?;
let mut zip = open_zip(bytes)?;
let (set_name, prefixes) = if zip.by_name("brushset.plist").is_ok() {
let buf = procreate::read_zip_entry(&mut zip, "brushset.plist").map_err(PreviewError)?;
let (name, uuids) = procreate::parse_brushset_plist(&buf).map_err(PreviewError)?;
(name, uuids.into_iter().map(|u| format!("{u}/")).collect())
} else {
let members: Vec<String> = procreate::members_in_zip_order(&mut zip)
.into_iter()
.map(|d| format!("{d}/"))
.collect();
if members.is_empty() {
return Err(PreviewError("no brushes found".to_string()));
}
(None, members)
};
let entries = prefixes
.iter()
.enumerate()
.map(|(index, prefix)| member_entry(&mut zip, index, prefix, max_cell))
.collect();
Ok(PreviewSet { set_name, entries })
}
pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
let max_cell = check_max_cell(opts)?;
let mut zip = open_zip(bytes)?;
if zip.by_name("Brush.archive").is_err() {
return Err(PreviewError("Brush.archive not found".to_string()));
}
Ok(PreviewSet {
set_name: None,
entries: vec![member_entry(&mut zip, 0, "", max_cell)],
})
}
fn open_zip(bytes: &[u8]) -> Result<zip::ZipArchive<Cursor<&[u8]>>, PreviewError> {
zip::ZipArchive::new(Cursor::new(bytes))
.map_err(|e| PreviewError(format!("failed to open zip: {e}")))
}
fn member_entry(
zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
index: usize,
prefix: &str,
max_cell: u32,
) -> PreviewEntry {
let fallback_name = if prefix.is_empty() {
"Brush".to_string()
} else {
prefix.trim_end_matches('/').to_string()
};
let archive = procreate::read_zip_entry(zip, &format!("{prefix}Brush.archive"))
.and_then(|buf| procreate::brush_name(&buf));
let name = match archive {
Ok(name) => name.unwrap_or(fallback_name),
Err(msg) => {
return PreviewEntry {
index,
name: fallback_name,
source_dimensions: None,
tip: TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
}
}
};
let shape_path = format!("{prefix}Shape.png");
if zip.by_name(&shape_path).is_err() {
return PreviewEntry {
index,
name,
tip: TipPreview::Unavailable(UnavailableReason::NoShapePng),
source_dimensions: None,
};
}
let mut source_dimensions = None;
let tip = match procreate::read_zip_entry(zip, &shape_path) {
Err(msg) => TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
Ok(png) => {
source_dimensions = procreate::header_dimensions(&png)
.and_then(|(width, height)| SourceDimensions::new(width, height));
match procreate::decode_tip_png(&png) {
Ok(bitmap) => TipPreview::Available(downsample(&bitmap, max_cell)),
Err(procreate::ShapePngError::TooLarge { width, height }) => {
TipPreview::Unavailable(UnavailableReason::TooLarge { width, height })
}
Err(procreate::ShapePngError::Corrupt(msg)) => {
TipPreview::Unavailable(UnavailableReason::Corrupt(msg))
}
}
}
};
PreviewEntry {
index,
name,
tip,
source_dimensions,
}
}