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, DeferredPack, 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)]
enum Take {
All,
FirstAvailable(usize),
}
impl Take {
fn collect(self, entries: impl Iterator<Item = PreviewEntry>) -> Vec<PreviewEntry> {
match self {
Take::All => entries.collect(),
Take::FirstAvailable(n) => entries
.filter(|entry| matches!(entry.tip, TipPreview::Available(_)))
.take(n)
.collect(),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Source {
Sampled(usize),
Computed(usize),
Unsupported(usize),
}
struct Row {
key: usize,
source: Source,
}
pub fn preview_abr(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
abr(bytes, opts, Take::All)
}
pub fn preview_abr_first_available(
bytes: &[u8],
opts: PreviewOptions,
n: usize,
) -> Result<PreviewSet, PreviewError> {
abr(bytes, opts, Take::FirstAvailable(n))
}
fn abr(bytes: &[u8], opts: PreviewOptions, take: Take) -> 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 pack = &deferred.pack;
let mut rows: Vec<Row> = Vec::new();
rows.extend(pack.brushes.iter().enumerate().map(|(i, brush)| Row {
key: brush.preset_index.unwrap_or(usize::MAX),
source: Source::Sampled(i),
}));
rows.extend(
pack.computed_presets
.iter()
.enumerate()
.map(|(i, preset)| Row {
key: preset.preset_index.unwrap_or(usize::MAX),
source: Source::Computed(i),
}),
);
rows.extend(
pack.unsupported_tip_presets
.iter()
.enumerate()
.map(|(i, preset)| Row {
key: preset.preset_index,
source: Source::Unsupported(i),
}),
);
rows.sort_by_key(|row| (row.key, row.source));
let entries = rows
.into_iter()
.enumerate()
.map(|(index, row)| abr_entry(&deferred, index, row.source, max_cell));
Ok(PreviewSet {
set_name: None,
entries: take.collect(entries),
})
}
fn abr_entry(deferred: &DeferredPack, index: usize, source: Source, max_cell: u32) -> PreviewEntry {
#[cfg(test)]
tests::record_entry();
let pack = &deferred.pack;
let (name, tip, source_dimensions) = match source {
Source::Sampled(i) => {
let brush = &pack.brushes[i];
let name = if brush.name.is_empty() {
brush.id.clone()
} else {
brush.name.clone()
};
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())),
};
(
name,
tip,
SourceDimensions::new(brush.tip.width, brush.tip.height),
)
}
Source::Computed(i) => {
let preset = &pack.computed_presets[i];
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(),
)),
};
(preset.name.clone(), tip, None)
}
Source::Unsupported(i) => {
let preset = &pack.unsupported_tip_presets[i];
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(),
};
(
preset.name.clone(),
TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(kind)),
None,
)
}
};
PreviewEntry {
index,
name,
tip,
source_dimensions,
}
}
pub fn preview_brushset(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
brushset(bytes, opts, Take::All)
}
pub fn preview_brushset_first_available(
bytes: &[u8],
opts: PreviewOptions,
n: usize,
) -> Result<PreviewSet, PreviewError> {
brushset(bytes, opts, Take::FirstAvailable(n))
}
fn brushset(bytes: &[u8], opts: PreviewOptions, take: Take) -> 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));
Ok(PreviewSet {
set_name,
entries: take.collect(entries),
})
}
pub fn preview_brush(bytes: &[u8], opts: PreviewOptions) -> Result<PreviewSet, PreviewError> {
brush(bytes, opts, Take::All)
}
pub fn preview_brush_first_available(
bytes: &[u8],
opts: PreviewOptions,
n: usize,
) -> Result<PreviewSet, PreviewError> {
brush(bytes, opts, Take::FirstAvailable(n))
}
fn brush(bytes: &[u8], opts: PreviewOptions, take: Take) -> 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()));
}
let entry = std::iter::once_with(|| member_entry(&mut zip, 0, "", max_cell));
Ok(PreviewSet {
set_name: None,
entries: take.collect(entry),
})
}
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 {
#[cfg(test)]
tests::record_entry();
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 shape_path = format!("{prefix}Shape.png");
let shape = if zip.by_name(&shape_path).is_ok() {
Some(procreate::read_zip_entry(zip, &shape_path))
} else {
None
};
let source_dimensions = match &shape {
Some(Ok(png)) => procreate::header_dimensions(png)
.and_then(|(width, height)| SourceDimensions::new(width, height)),
_ => None,
};
let (name, tip) = match archive {
Ok(name) => (name.unwrap_or(fallback_name), shape_tip(shape, max_cell)),
Err(msg) => (
fallback_name,
TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
),
};
PreviewEntry {
index,
name,
tip,
source_dimensions,
}
}
fn shape_tip(shape: Option<Result<Vec<u8>, String>>, max_cell: u32) -> TipPreview {
let png = match shape {
None => return TipPreview::Unavailable(UnavailableReason::NoShapePng),
Some(Err(msg)) => return TipPreview::Unavailable(UnavailableReason::Corrupt(msg)),
Some(Ok(png)) => png,
};
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))
}
}
}
#[cfg(test)]
#[path = "../tests/common/mod.rs"]
mod common;
#[cfg(test)]
mod tests {
use super::*;
use crate::common::{brush_archive, brushset_plist, gray_png, samp_abr, zip_with, SampTip};
use std::cell::Cell;
thread_local! {
static ENTRIES: Cell<usize> = const { Cell::new(0) };
}
pub(super) fn record_entry() {
ENTRIES.with(|count| count.set(count.get() + 1));
}
fn entries_built<T>(f: impl FnOnce() -> T) -> usize {
ENTRIES.with(|count| count.set(0));
f();
ENTRIES.with(Cell::get)
}
const OPTS: PreviewOptions = PreviewOptions { max_cell: 8 };
fn tip(corrupt: bool) -> SampTip {
SampTip {
width: 4,
height: 4,
fill: 0x80,
corrupt,
}
}
#[test]
fn abr_builds_only_the_entries_it_returns() {
let bytes = samp_abr(&[
tip(false),
tip(false),
tip(false),
tip(false),
tip(false),
tip(false),
]);
assert_eq!(
entries_built(|| preview_abr_first_available(&bytes, OPTS, 2).unwrap()),
2
);
assert_eq!(
entries_built(|| preview_abr_first_available(&bytes, OPTS, 0).unwrap()),
0
);
assert_eq!(entries_built(|| preview_abr(&bytes, OPTS).unwrap()), 6);
}
#[test]
fn abr_failed_decode_does_not_count_toward_n() {
let bytes = samp_abr(&[tip(false), tip(false), tip(true)]);
assert!(matches!(
preview_abr(&bytes, OPTS).unwrap().entries[0].tip,
TipPreview::Unavailable(UnavailableReason::Corrupt(_))
));
let mut set = None;
assert_eq!(
entries_built(|| set = Some(preview_abr_first_available(&bytes, OPTS, 1).unwrap())),
2
);
let entries = set.unwrap().entries;
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].index, 1);
}
#[test]
fn brushset_reads_only_the_members_it_returns() {
let archive = brush_archive("Tip");
let shape = gray_png(4, 4, 200);
let plist = brushset_plist("Set", &["a", "b", "c", "d"]);
let mut files: Vec<(String, &[u8])> = vec![("brushset.plist".into(), &plist)];
for member in ["a", "b", "c", "d"] {
files.push((format!("{member}/Brush.archive"), &archive));
files.push((format!("{member}/Shape.png"), &shape));
}
let files: Vec<(&str, &[u8])> = files.iter().map(|(p, b)| (p.as_str(), *b)).collect();
let bytes = zip_with(&files);
assert_eq!(
entries_built(|| preview_brushset_first_available(&bytes, OPTS, 1).unwrap()),
1
);
assert_eq!(entries_built(|| preview_brushset(&bytes, OPTS).unwrap()), 4);
}
}