brushkit-preview 0.7.0

Tip bitmaps for .abr, .brush and .brushset files: sampled tips, synthesized computed tips, Procreate Shape.png and contact sheets
Documentation
mod common;

use brushkit_preview::{
    preview_abr, preview_brush, preview_brushset, PreviewOptions, SourceDimensions, TipPreview,
    UnavailableReason,
};
use common::{brush_archive, brushset_plist, corpus_files, dimension_bomb_png, gray_png, zip_with};
use std::path::Path;

#[test]
fn dimensions_require_two_positive_sides() {
    assert_eq!(SourceDimensions::new(0, 1), None);
    assert_eq!(SourceDimensions::new(1, 0), None);
    let dimensions = SourceDimensions::new(16, 8).unwrap();
    assert_eq!((dimensions.width(), dimensions.height()), (16, 8));
}

#[test]
fn sampled_abr_dimensions_survive_downsampling() {
    let bytes = include_bytes!("../../../fuzz/corpus/preview_abr/sampled_tip");
    for max_cell in [1, 4, 64] {
        let set = preview_abr(bytes, PreviewOptions { max_cell }).unwrap();
        assert_eq!(set.entries.len(), 1);
        assert_eq!(
            set.entries[0].source_dimensions,
            SourceDimensions::new(16, 8)
        );
        assert!(matches!(set.entries[0].tip, TipPreview::Available(_)));
    }
}

#[test]
fn sampled_dimensions_survive_decode_failure() {
    let mut bytes = include_bytes!("../../../fuzz/corpus/preview_abr/sampled_tip").to_vec();
    bytes[42] = 1; // RLE, with row lengths larger than the remaining payload.
    let set = preview_abr(&bytes, PreviewOptions { max_cell: 8 }).unwrap();
    assert_eq!(set.entries.len(), 1);
    assert_eq!(
        set.entries[0].source_dimensions,
        SourceDimensions::new(16, 8)
    );
    assert!(matches!(
        set.entries[0].tip,
        TipPreview::Unavailable(UnavailableReason::Corrupt(_))
    ));
}

#[test]
fn procreate_dimensions_follow_members_and_survive_pixel_failures() {
    let archive = brush_archive("Same name");
    let shape = gray_png(64, 16, 200);
    // Retain IHDR and the IDAT header, but remove the compressed pixel data.
    let idat = shape.windows(4).position(|bytes| bytes == b"IDAT").unwrap();
    let corrupt_pixels = &shape[..idat + 4];
    let plist = brushset_plist(
        "Sizes",
        &[
            "missing", "valid", "pixels", "header", "large", "archive", "nothing",
        ],
    );
    let bytes = zip_with(&[
        ("brushset.plist", &plist),
        ("valid/Brush.archive", &archive),
        ("valid/Shape.png", &shape),
        ("missing/Brush.archive", &archive),
        ("pixels/Brush.archive", &archive),
        ("pixels/Shape.png", corrupt_pixels),
        ("header/Brush.archive", &archive),
        ("header/Shape.png", b"bad header"),
        ("large/Brush.archive", &archive),
        ("large/Shape.png", &dimension_bomb_png(60000, 30000)),
        ("archive/Brush.archive", b"bad archive"),
        ("archive/Shape.png", &shape),
        ("nothing/Brush.archive", b"bad archive"),
    ]);
    for max_cell in [1, 8, 128] {
        let set = preview_brushset(&bytes, PreviewOptions { max_cell }).unwrap();
        assert_eq!(
            set.entries
                .iter()
                .map(|entry| entry.index)
                .collect::<Vec<_>>(),
            vec![0, 1, 2, 3, 4, 5, 6]
        );
        assert_eq!(
            set.entries
                .iter()
                .map(|entry| entry.source_dimensions)
                .collect::<Vec<_>>(),
            vec![
                None,
                SourceDimensions::new(64, 16),
                SourceDimensions::new(64, 16),
                None,
                SourceDimensions::new(60000, 30000),
                SourceDimensions::new(64, 16),
                None
            ]
        );
        assert!(matches!(
            set.entries[0].tip,
            TipPreview::Unavailable(UnavailableReason::NoShapePng)
        ));
        assert!(matches!(set.entries[1].tip, TipPreview::Available(_)));
        assert!(matches!(
            set.entries[2].tip,
            TipPreview::Unavailable(UnavailableReason::Corrupt(_))
        ));
        assert!(matches!(
            set.entries[4].tip,
            TipPreview::Unavailable(UnavailableReason::TooLarge {
                width: 60000,
                height: 30000
            })
        ));
        for archive_error in &set.entries[5..] {
            assert!(matches!(
                archive_error.tip,
                TipPreview::Unavailable(UnavailableReason::Corrupt(_))
            ));
        }
    }
}

#[test]
fn computed_and_unsupported_previews_have_no_source_raster() {
    // One computed preset, with a diameter and otherwise default geometry.
    let mut descriptor = vec![0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0];
    descriptor.extend_from_slice(b"null\0\0\0\x01\0\0\0\0BrshVlLs\0\0\0\x01Objc\0\0\0\x01\0\0\0\0\0\x0bbrushPreset\0\0\0\x01\0\0\0\0BrshObjc\0\0\0\x01\0\0\0\0\0\x0dcomputedBrush\0\0\0\x01\0\0\0\0DmtrUntF#Pxl");
    descriptor.extend_from_slice(&30f64.to_be_bytes());
    let mut bytes = vec![0, 6, 0, 2];
    bytes.extend_from_slice(b"8BIMdesc");
    bytes.extend_from_slice(&(descriptor.len() as u32).to_be_bytes());
    bytes.extend_from_slice(&descriptor);
    bytes.resize(bytes.len().next_multiple_of(4), 0);
    let set = preview_abr(&bytes, PreviewOptions { max_cell: 8 }).unwrap();
    assert_eq!(set.entries.len(), 1);
    assert_eq!(set.entries[0].source_dimensions, None);
    assert!(matches!(set.entries[0].tip, TipPreview::Available(_)));

    let class = bytes
        .windows(13)
        .position(|value| value == b"computedBrush")
        .unwrap();
    bytes[class..class + 13].copy_from_slice(b"computedBrusX");
    let set = preview_abr(&bytes, PreviewOptions { max_cell: 8 }).unwrap();
    assert_eq!(set.entries.len(), 1);
    assert_eq!(set.entries[0].source_dimensions, None);
    assert!(matches!(
        set.entries[0].tip,
        TipPreview::Unavailable(UnavailableReason::UnsupportedTipKind(_))
    ));
}

#[test]
fn real_file_dimensions_are_independent_of_cell_size() {
    let Some(root) = std::env::var_os("BRUSHKIT_CORPUS_DIR") else {
        return;
    };
    let mut files = Vec::new();
    corpus_files(Path::new(&root), &mut files);
    assert!(
        !files.is_empty(),
        "configured corpus must contain brush files"
    );
    let mut known = 0;
    for path in files {
        let bytes = std::fs::read(&path).unwrap();
        let reader = match path
            .extension()
            .unwrap()
            .to_str()
            .unwrap()
            .to_lowercase()
            .as_str()
        {
            "abr" => preview_abr,
            "brush" => preview_brush,
            _ => preview_brushset,
        };
        let large = match reader(&bytes, PreviewOptions { max_cell: 160 }) {
            Ok(set) => set,
            Err(error) => {
                println!("skipped {}: {error}", path.display());
                continue;
            }
        };
        let small = reader(&bytes, PreviewOptions { max_cell: 8 })
            .unwrap_or_else(|error| panic!("{}: {error}", path.display()));
        assert_eq!(small.entries.len(), large.entries.len());
        for (small, large) in small.entries.iter().zip(&large.entries) {
            assert_eq!(small.name, large.name);
            assert_eq!(small.index, large.index);
            assert_eq!(small.source_dimensions, large.source_dimensions);
            if large.source_dimensions.is_some() {
                known += 1;
            }
            for (entry, max_cell) in [(small, 8), (large, 160)] {
                let (Some(dimensions), TipPreview::Available(bitmap)) =
                    (entry.source_dimensions, &entry.tip)
                else {
                    continue;
                };
                let source = (dimensions.width(), dimensions.height());
                let preview = (bitmap.width, bitmap.height);
                if source.0 <= max_cell && source.1 <= max_cell {
                    assert_eq!(source, preview, "{} #{}", path.display(), entry.index);
                } else {
                    assert_eq!(
                        preview.0.max(preview.1),
                        max_cell,
                        "{} #{}",
                        path.display(),
                        entry.index
                    );
                    assert!(source.0 >= preview.0 && source.1 >= preview.1);
                }
            }
        }
        println!(
            "checked {} entries in {}",
            large.entries.len(),
            path.display()
        );
    }
    assert!(known > 0, "corpus must exercise known raster dimensions");
}