mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
File formats: .dib and .rle bitmaps, .pdd Photoshop files and .pns PNGs open and place
File > Open and File > Place read BMP files named .dib or .rle (run-length encoded bitmaps included), Photoshop files named .pdd and PNG files named .pns, in any letter case. The Open and Place dialogs list them in "All readable files", "All placeable files" and their format's own filter, and Glass's Load Texture lists them too. Linked files with these names update and relink, the Links panel shows their format, and Edit Original opens them. The extensions are read-only: exports still write .bmp, .psd and .png, and an export to a path with one of the new extensions is refused as before. The desktop packaging does not associate them with the app.
This commit is contained in:
@@ -127,8 +127,8 @@ use export_names::{distinct_names_by, write_encoded};
|
||||
use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, import_name, writable};
|
||||
pub use formats::{
|
||||
DOC_EXTS, DOCX_EXTS, FORMATS, Format, FormatOption, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS, UNASSOCIATED_EXTS, export_extensions, extension,
|
||||
file_stem, format, format_for_name, has_own_title, library_open_filters, open_filters, place_filters, plugin_formats, titled_for_export,
|
||||
writable_format,
|
||||
file_stem, format, format_for_name, has_own_title, library_open_filters, open_filters, place_filters, plugin_formats, read_extensions,
|
||||
readable_format, readable_format_for_name, titled_for_export, writable_format,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -508,6 +508,7 @@ pub const OPEN_EXTS: &[&str] = &[
|
||||
"ai",
|
||||
"ait",
|
||||
"png",
|
||||
"pns",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
@@ -515,6 +516,8 @@ pub const OPEN_EXTS: &[&str] = &[
|
||||
"tif",
|
||||
"tiff",
|
||||
"bmp",
|
||||
"dib",
|
||||
"rle",
|
||||
"tga",
|
||||
"vda",
|
||||
"icb",
|
||||
@@ -527,6 +530,7 @@ pub const OPEN_EXTS: &[&str] = &[
|
||||
"j2c",
|
||||
"jpc",
|
||||
"psd",
|
||||
"pdd",
|
||||
"psb",
|
||||
"vctemplate",
|
||||
"dxf",
|
||||
@@ -542,10 +546,11 @@ pub const OPEN_EXTS: &[&str] = &[
|
||||
|
||||
/// Extensions in [`OPEN_EXTS`] the packaging declares no type for: Photoshop documents belong to
|
||||
/// raster editors, so Finder and file managers don't offer a vector app for them, and there is no
|
||||
/// `.epsf`, `.ps`, Targa, PCX or JPEG 2000 type (File › Open and Place still read them all). On Linux the
|
||||
/// desktop entry's EPS type (shared-mime-info's) also covers `.epsf`, so file managers offer
|
||||
/// VectorCraft for it.
|
||||
pub const UNASSOCIATED_EXTS: &[&str] = &["psd", "psb", "epsf", "ps", "tga", "vda", "icb", "vst", "pcx", "jpf", "jpx", "jp2", "j2k", "j2c", "jpc"];
|
||||
/// `.epsf`, `.ps`, Targa, PCX or JPEG 2000 type, nor one for the `.pns`, `.dib` and `.rle` names of
|
||||
/// PNG and BMP files (File › Open and Place still read them all). On Linux the desktop entry's EPS
|
||||
/// type (shared-mime-info's) also covers `.epsf`, so file managers offer VectorCraft for it.
|
||||
pub const UNASSOCIATED_EXTS: &[&str] =
|
||||
&["psd", "pdd", "psb", "epsf", "ps", "pns", "dib", "rle", "tga", "vda", "icb", "vst", "pcx", "jpf", "jpx", "jp2", "j2k", "j2c", "jpc"];
|
||||
|
||||
/// The extension that picks each writable format when exporting (the format's first; PNG-8 shares
|
||||
/// `.png` with PNG, so `.png` comes once), in [`FORMATS`] order.
|
||||
@@ -586,6 +591,7 @@ pub const PLACE_EXTS: &[&str] = &[
|
||||
"ai",
|
||||
"ait",
|
||||
"png",
|
||||
"pns",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
@@ -593,6 +599,8 @@ pub const PLACE_EXTS: &[&str] = &[
|
||||
"tif",
|
||||
"tiff",
|
||||
"bmp",
|
||||
"dib",
|
||||
"rle",
|
||||
"tga",
|
||||
"vda",
|
||||
"icb",
|
||||
@@ -605,6 +613,7 @@ pub const PLACE_EXTS: &[&str] = &[
|
||||
"j2c",
|
||||
"jpc",
|
||||
"psd",
|
||||
"pdd",
|
||||
"psb",
|
||||
"vctemplate",
|
||||
"dxf",
|
||||
@@ -622,9 +631,32 @@ pub const PLACE_EXTS: &[&str] = &[
|
||||
"doc",
|
||||
];
|
||||
|
||||
/// The extensions File › Open and Place read PNG, BMP and Photoshop files by: the format's own,
|
||||
/// then the other names its files have (`.pns`, `.dib` and `.rle`, `.pdd`). Exports write only
|
||||
/// the format's own extension.
|
||||
const READ_ALIASES: &[(&str, &[&str])] = &[("png", &["png", "pns"]), ("bmp", &["bmp", "dib", "rle"]), ("psd", &["psd", "pdd"])];
|
||||
|
||||
/// The extensions File › Open and Place read format `f` by: its own, and for PNG, BMP and PSD
|
||||
/// those of [`READ_ALIASES`].
|
||||
pub fn read_extensions(f: &Format) -> &'static [&'static str] {
|
||||
READ_ALIASES.iter().find(|(id, _)| *id == f.id).map_or(f.extensions, |(_, exts)| *exts)
|
||||
}
|
||||
|
||||
/// The readable format of extension `ext` (any case, leading dot allowed): [`format`]'s when it
|
||||
/// reads, else the one [`READ_ALIASES`] gives it (`.dib` is BMP).
|
||||
pub fn readable_format(ext: &str) -> Option<&'static Format> {
|
||||
let k = ext.trim_start_matches('.').to_ascii_lowercase();
|
||||
format(&k).filter(|f| f.read).or_else(|| READ_ALIASES.iter().find(|(_, exts)| exts.contains(&k.as_str())).and_then(|(id, _)| format(id)))
|
||||
}
|
||||
|
||||
/// The readable format a file name's extension names ([`readable_format`]).
|
||||
pub fn readable_format_for_name(name: &str) -> Option<&'static Format> {
|
||||
Some(extension(name)).filter(|e| !e.is_empty()).and_then(|e| readable_format(&e))
|
||||
}
|
||||
|
||||
/// One dialog filter per readable format.
|
||||
fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
FORMATS.iter().filter(|f| f.read).map(|f| (f.label, f.extensions))
|
||||
FORMATS.iter().filter(|f| f.read).map(|f| (f.label, read_extensions(f)))
|
||||
}
|
||||
|
||||
/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
|
||||
|
||||
@@ -9,7 +9,7 @@ use vectorcraft_render::effects::pixel::LoadedTexture;
|
||||
|
||||
use super::super::{Result, Session, bad, bool_or, clipboard, parse_range, str_param};
|
||||
use super::pdfimport::LoadOptions;
|
||||
use super::{Format, SAVE_FORMATS, absolute_path, file_stamp, format, format_for_name, psdread, read_file};
|
||||
use super::{Format, SAVE_FORMATS, absolute_path, file_stamp, format, psdread, read_file, readable_format_for_name};
|
||||
use crate::cmd::links::Resolved;
|
||||
use crate::{EngineError, Prefs};
|
||||
|
||||
@@ -67,7 +67,7 @@ pub fn file_name(name: &str) -> String {
|
||||
/// The readable format of a file: from its content (magic bytes) when that tells, else from the
|
||||
/// extension of `name`.
|
||||
pub fn detect(name: &str, bytes: &[u8]) -> Option<&'static Format> {
|
||||
let by_name = format_for_name(name).filter(|f| f.read);
|
||||
let by_name = readable_format_for_name(name);
|
||||
if vectorcraft_affinity::is_affinity(bytes) {
|
||||
return format("affinity");
|
||||
}
|
||||
|
||||
@@ -366,12 +366,12 @@ fn templates_open_untitled() {
|
||||
#[test]
|
||||
fn open_exts_cover_every_readable_format() {
|
||||
for f in FORMATS.iter().filter(|f| f.read) {
|
||||
for e in f.extensions {
|
||||
for e in read_extensions(f) {
|
||||
assert!(OPEN_EXTS.contains(e), "OPEN_EXTS lacks .{e} ({})", f.label);
|
||||
}
|
||||
}
|
||||
for e in OPEN_EXTS {
|
||||
assert!(format(e).is_some_and(|f| f.read), ".{e} in OPEN_EXTS is no readable format");
|
||||
assert!(readable_format(e).is_some(), ".{e} in OPEN_EXTS is no readable format");
|
||||
}
|
||||
let filters: Vec<_> = open_filters().collect();
|
||||
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
|
||||
|
||||
@@ -213,6 +213,28 @@ fn psd_exports_open_and_place_again() {
|
||||
assert!(PLACE_EXTS.contains(&"psd") && PLACE_EXTS.contains(&"psb") && OPEN_EXTS.contains(&"psb"));
|
||||
}
|
||||
|
||||
/// Photoshop files named `.pdd` open and place as PSD, in any case; the dialogs and a drop on the
|
||||
/// window take them by their extension, and the desktop packaging doesn't associate them.
|
||||
#[test]
|
||||
fn pdd_files_open_and_place() {
|
||||
let mut s = art();
|
||||
let f = exported(&mut s, json!({"format": "psd"}));
|
||||
for name in ["art.pdd", "ART.PDD"] {
|
||||
assert_eq!(readable_format_for_name(name).map(|f| f.id), Some("psd"), "{name}");
|
||||
let r = s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&f)})).unwrap();
|
||||
assert_eq!((&r["format"], &r["warnings"]), (&json!("psd"), &json!([])), "{name}: {r}");
|
||||
assert!((s.doc().unwrap().doc.artboards[0].rect.width() - 60.0).abs() < 1e-6, "{name}");
|
||||
s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&f)})).unwrap();
|
||||
assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!("psd"), Some(1)), "{name}: {r}");
|
||||
}
|
||||
assert!(OPEN_EXTS.contains(&"pdd") && PLACE_EXTS.contains(&"pdd") && UNASSOCIATED_EXTS.contains(&"pdd"));
|
||||
let photoshop = ("PSD", &["psd", "pdd"][..]);
|
||||
assert!(open_filters().any(|f| f == photoshop) && place_filters().any(|f| f == photoshop));
|
||||
assert!(!export_extensions().contains(&"pdd"), "exports still name their files .psd");
|
||||
assert!(format_for_name("x.pdd").is_none() && writable_format(None, Some("x.pdd")).is_err(), "an export to .pdd is refused as before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_and_unsupported_files_are_refused_without_a_panic() {
|
||||
let good = psd(2, 4, (3, 2), 8, 3, &[], &[], &layer_count(-1, true), &raw(&[&[1; 6], &[2; 6], &[3; 6], &[4; 6]]));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! TIFF, BMP and Targa export through `document.export`: the options reach the files, impossible
|
||||
//! ones are refused, and the files open again; Targa files open and place.
|
||||
//! ones are refused, and the files open again; Targa files, and BMP and PNG files by their other
|
||||
//! extensions, open and place.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -203,6 +204,52 @@ fn targa_files_open_and_place() {
|
||||
assert!(open_filters().any(|f| f == targa) && place_filters().any(|f| f == targa));
|
||||
}
|
||||
|
||||
/// BMP files named `.dib` or `.rle` (an RLE8 bitmap) and PNG files named `.pns` open and place as
|
||||
/// BMP and PNG, in any case; the dialogs and a drop on the window take them by their extension, and
|
||||
/// the desktop packaging doesn't associate them.
|
||||
#[test]
|
||||
fn dib_rle_and_pns_files_open_and_place() {
|
||||
let mut s = session();
|
||||
let b64 = vectorcraft_format::base64_encode;
|
||||
let bmp = export(&mut s, json!({"format": "bmp"}));
|
||||
let rle = export(&mut s, json!({"format": "bmp", "depth": 8, "rle": true}));
|
||||
assert_eq!(u32::from_le_bytes(rle[30..34].try_into().unwrap()), 1, "RLE8");
|
||||
let png = export(&mut s, json!({"format": "png"}));
|
||||
for (name, bytes, id) in [
|
||||
("art.dib", &bmp, "bmp"),
|
||||
("ART.DIB", &bmp, "bmp"),
|
||||
("art.rle", &rle, "bmp"),
|
||||
("Art.RLE", &rle, "bmp"),
|
||||
("art.pns", &png, "png"),
|
||||
("ART.PNS", &png, "png"),
|
||||
] {
|
||||
assert_eq!(readable_format_for_name(name).map(|f| f.id), Some(id), "{name}");
|
||||
let r = s.execute("document.open", &json!({"name": name, "dataBase64": b64(bytes)})).unwrap();
|
||||
assert_eq!((&r["format"], &r["warnings"]), (&json!(id), &json!([])), "{name}: {r}");
|
||||
assert_eq!(stored_pixels(&s).dimensions(), (60, 40), "{name}");
|
||||
s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap();
|
||||
let r = s.execute("file.place", &json!({"name": name, "dataBase64": b64(bytes)})).unwrap();
|
||||
assert_eq!((&r["format"], r["ids"].as_array().map(Vec::len)), (&json!(id), Some(1)), "{name}: {r}");
|
||||
let black = stored_pixels(&s).get_pixel(40, 20).0;
|
||||
assert!(black[3] == 255 && black[..3].iter().all(|c| *c < 80), "{name}: {black:?}");
|
||||
}
|
||||
// The dialogs list the extensions; the desktop packaging doesn't associate them.
|
||||
for e in ["dib", "rle", "pns"] {
|
||||
assert!(OPEN_EXTS.contains(&e) && PLACE_EXTS.contains(&e) && UNASSOCIATED_EXTS.contains(&e), "{e}");
|
||||
}
|
||||
let (bitmap, png) = (("BMP", &["bmp", "dib", "rle"][..]), ("PNG", &["png", "pns"][..]));
|
||||
assert!(open_filters().any(|f| f == bitmap) && place_filters().any(|f| f == bitmap));
|
||||
assert!(open_filters().any(|f| f == png) && place_filters().any(|f| f == png));
|
||||
// Exports still write `.png` and `.bmp` only: an export to one of the other names is refused
|
||||
// as before, and save panels don't offer them.
|
||||
let named: Vec<&str> = export_extensions().into_iter().filter(|e| ["bmp", "dib", "rle", "png", "pns"].contains(e)).collect();
|
||||
assert_eq!(named, ["png", "bmp"]);
|
||||
for name in ["x.dib", "x.rle", "x.pns"] {
|
||||
assert!(format_for_name(name).is_none() && writable_format(None, Some(name)).is_err(), "{name}");
|
||||
assert!(s.execute("document.export", &json!({"path": name})).is_err(), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Targa pixels in every layout: run-length encoded rows stored top down (as the `image` crate
|
||||
/// writes them), a 16-bit file whose one attribute bit per pixel is its alpha, with runs, stored
|
||||
/// right to left and top down, 32-bit files with no attribute bits (opaque), with eight, and with
|
||||
|
||||
@@ -335,7 +335,8 @@ pub(crate) fn linked_files(d: &Document, doc_path: Option<&str>) -> Vec<LinkedFi
|
||||
.into_iter()
|
||||
.filter(|g| seen.insert(g.link.path.clone()))
|
||||
.map(|g| {
|
||||
let own = |b: &&ImageBlob| !b.is_proxy() && fileio::format_for_name(g.link.name()).is_some_and(|f| f.mime == b.mime);
|
||||
let named = fileio::format_for_name(g.link.name()).or_else(|| fileio::readable_format_for_name(g.link.name()));
|
||||
let own = |b: &&ImageBlob| !b.is_proxy() && named.is_some_and(|f| f.mime == b.mime);
|
||||
let found = locate(&g.link, doc_path);
|
||||
let found_path = found.as_ref().map(|f| f.path.clone());
|
||||
let bytes = found.and_then(|mut f| f.read().ok()).or_else(|| d.images.get(&g.key).filter(own).map(|b| b.bytes.to_vec()));
|
||||
@@ -1124,7 +1125,7 @@ fn statuses(st: &crate::DocState) -> BTreeMap<NodeId, (Status, Option<String>, b
|
||||
fn format_label(d: &Document, im: &ImageObject) -> &'static str {
|
||||
im.link
|
||||
.as_ref()
|
||||
.and_then(|l| fileio::format_for_name(l.name()))
|
||||
.and_then(|l| fileio::format_for_name(l.name()).or_else(|| fileio::readable_format_for_name(l.name())))
|
||||
.or_else(|| d.images.get(&im.key).and_then(|b| fileio::FORMATS.iter().find(|f| f.raster && f.mime == b.mime)))
|
||||
.map_or("Image", |f| f.label)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user