Add shared format capability catalog

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 04:53:57 -05:00
1 parent 6417d6b90a
commit 6de1ffc9d7
3 files changed
+391 -48

No files matched your search

Generated
+6 -5
View File
@@ -1572,6 +1572,7 @@ version = "0.1.0"
dependencies = [
"craft-eps",
"craft-idml",
"craft-image-codecs",
"craft-svg",
"craft-zip",
"serde",
@@ -4941,17 +4942,17 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65b27460c2c92b037f3f94c538ed9a3342f3fdf923606781629ccb35f82d042a"
[[package]]
name = "indd"
version = "0.1.0"
source = "git+https://github.com/storytold/indd-utils?rev=7f3060e5bce78d21ea70e0d5eacb96edce15dfbe#7f3060e5bce78d21ea70e0d5eacb96edce15dfbe"
[[package]]
name = "imgref"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f"
[[package]]
name = "indd"
version = "0.1.0"
source = "git+https://github.com/storytold/indd-utils?rev=7f3060e5bce78d21ea70e0d5eacb96edce15dfbe#7f3060e5bce78d21ea70e0d5eacb96edce15dfbe"
[[package]]
name = "indexmap"
version = "2.14.2"
+24
View File
@@ -9,11 +9,35 @@ rust-version.workspace = true
[dependencies]
craft-eps = { workspace = true }
craft-image-codecs = { workspace = true, optional = true }
craft-idml = { workspace = true }
craft-svg = { workspace = true }
craft-zip = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
[features]
default = []
# These flags report which shared parser/codec families a client builds. The application still
# selects the corresponding shared crates in its dependency graph.
raster-codecs = ["dep:craft-image-codecs"]
heif = ["raster-codecs", "craft-image-codecs/heif"]
avif = ["raster-codecs", "craft-image-codecs/avif"]
pdf = []
svg = []
ora = []
kra = []
xcf = []
pdn = []
raw = []
psd = []
affinity = []
pcraft = []
ase = []
office = []
designcraft = []
stencilcraft = []
plotter = []
[lints]
workspace = true
+361 -43
View File
@@ -23,6 +23,22 @@ pub enum Format {
Tiff,
WebP,
Heif,
Tga,
Ico,
Pnm,
Pcx,
Xpm,
Dds,
Qoi,
OpenExr,
Hdr,
Avif,
Fits,
Dpx,
Jxl,
Cineon,
Dicom,
Iff,
Ase,
Psd,
Affinity,
@@ -54,6 +70,349 @@ pub enum Format {
StencilCraft,
}
/// Availability of one operation in the current client's shared-format build.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Availability {
/// The shared reader or writer is included in this build.
Available,
/// This format is known, but the corresponding shared operation is not implemented.
Unsupported,
/// The operation exists behind a Cargo feature that this client did not enable.
BuildDisabled,
}
/// Shared reader/writer capability and sequence behavior for a known format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities {
pub read: Availability,
pub write: Availability,
/// Input may contain multiple frames/pages that the shared reader can expose individually.
pub multiple_frames: bool,
/// This format can be opened as an ordered sequence of files by an application adapter.
pub file_sequence: bool,
}
const fn capability(feature: bool, read: bool, write: bool, multiple_frames: bool, file_sequence: bool) -> Capabilities {
Capabilities { read: availability(feature, read), write: availability(feature, write), multiple_frames, file_sequence }
}
const fn availability(feature: bool, supported: bool) -> Availability {
if !supported {
Availability::Unsupported
} else if feature {
Availability::Available
} else {
Availability::BuildDisabled
}
}
impl Format {
/// Every known format, including formats whose reader or writer is not enabled in this build.
pub const ALL: [Format; 56] = [
Self::Pdf,
Self::Eps,
Self::PostScript,
Self::Illustrator,
Self::Dxf,
Self::Svg,
Self::Svgz,
Self::Png,
Self::Jpeg,
Self::Bmp,
Self::Gif,
Self::Tiff,
Self::WebP,
Self::Heif,
Self::Tga,
Self::Ico,
Self::Pnm,
Self::Pcx,
Self::Xpm,
Self::Dds,
Self::Qoi,
Self::OpenExr,
Self::Hdr,
Self::Avif,
Self::Fits,
Self::Dpx,
Self::Jxl,
Self::Cineon,
Self::Dicom,
Self::Iff,
Self::Ase,
Self::Psd,
Self::Affinity,
Self::Ora,
Self::Kra,
Self::Xcf,
Self::Pdn,
Self::Raw,
Self::Pcraft,
Self::Idml,
Self::DesignCraft,
Self::DesignCraftJson,
Self::Indd,
Self::Docx,
Self::LegacyDoc,
Self::Rtf,
Self::Xlsx,
Self::TaggedText,
Self::Pptx,
Self::PlainText,
Self::Csv,
Self::Tsv,
Self::LegacyExcel,
Self::Studio,
Self::StencilCraft,
Self::Gcode,
];
/// Known extensions in lowercase form, in preferred-output order.
pub fn extensions(self) -> &'static [&'static str] {
match self {
Self::Pdf => &["pdf"],
Self::Eps => &["eps"],
Self::PostScript => &["ps"],
Self::Illustrator => &["ai"],
Self::Dxf => &["dxf"],
Self::Svg => &["svg"],
Self::Svgz => &["svgz"],
Self::Png => &["png", "apng"],
Self::Jpeg => &["jpg", "jpeg", "jpe", "jfif"],
Self::Bmp => &["bmp", "dib"],
Self::Gif => &["gif"],
Self::Tiff => &["tif", "tiff"],
Self::WebP => &["webp"],
Self::Heif => &["heic", "heif", "hif"],
Self::Tga => &["tga", "icb", "vda", "vst"],
Self::Ico => &["ico"],
Self::Pnm => &["pnm", "pbm", "pgm", "ppm", "pam", "pfm"],
Self::Pcx => &["pcx"],
Self::Xpm => &["xpm"],
Self::Dds => &["dds"],
Self::Qoi => &["qoi"],
Self::OpenExr => &["exr"],
Self::Hdr => &["hdr"],
Self::Avif => &["avif"],
Self::Fits => &["fits", "fit", "fts"],
Self::Dpx => &["dpx"],
Self::Jxl => &["jxl"],
Self::Cineon => &["cin"],
Self::Dicom => &["dcm", "dicom", "dc3"],
Self::Iff => &["iff", "lbm", "ilbm"],
Self::Ase => &["ase"],
Self::Psd => &["psd", "psb"],
Self::Affinity => &["afdesign", "afphoto", "afpub"],
Self::Ora => &["ora"],
Self::Kra => &["kra"],
Self::Xcf => &["xcf"],
Self::Pdn => &["pdn"],
Self::Raw => &[
"dng", "3fr", "ari", "arw", "bay", "cap", "cr2", "cr3", "dcs", "dcr", "drf", "eip", "erf", "fff", "gpr", "iiq", "k25", "kdc", "mdc",
"mef", "mos", "mrw", "nef", "nrw", "obm", "orf", "pef", "ptx", "pxn", "r3d", "raf", "raw", "rwl", "rw2", "rwz", "sr2", "srf", "sti",
"x3f",
],
Self::Pcraft => &["pcraft"],
Self::Idml => &["idml"],
Self::DesignCraft => &["designcraft", "dcdoc"],
Self::DesignCraftJson => &[],
Self::Indd => &["indd", "indt"],
Self::Docx => &["docx"],
Self::LegacyDoc => &["doc"],
Self::Rtf => &["rtf"],
Self::Xlsx => &["xlsx"],
Self::TaggedText => &[],
Self::Pptx => &["pptx"],
Self::PlainText => &["txt", "text", "md"],
Self::Csv => &["csv"],
Self::Tsv => &["tsv", "tab"],
Self::LegacyExcel => &["xls", "xlsm"],
Self::Studio => &["studio", "studio3"],
Self::StencilCraft => &["stencilcraft"],
Self::Gcode => &["gcode", "nc"],
}
}
/// Preferred extension for creating a new file of this type, if one is defined.
pub fn preferred_extension(self) -> Option<&'static str> {
self.extensions().first().copied()
}
/// Registered MIME type, where the format has a stable MIME registration.
pub fn mime_type(self) -> Option<&'static str> {
Some(match self {
Self::Pdf => "application/pdf",
Self::Eps => "application/postscript",
Self::PostScript => "application/postscript",
Self::Illustrator => "application/illustrator",
Self::Dxf => "image/vnd.dxf",
Self::Svg | Self::Svgz => "image/svg+xml",
Self::Png => "image/png",
Self::Jpeg => "image/jpeg",
Self::Bmp => "image/bmp",
Self::Gif => "image/gif",
Self::Tiff => "image/tiff",
Self::WebP => "image/webp",
Self::Heif => "image/heif",
Self::Tga => "image/x-tga",
Self::Ico => "image/vnd.microsoft.icon",
Self::Pnm => "image/x-portable-anymap",
Self::Pcx => "image/vnd.zbrush.pcx",
Self::Xpm => "image/x-xpixmap",
Self::Dds => "image/vnd-ms.dds",
Self::Qoi => "image/qoi",
Self::OpenExr => "image/x-exr",
Self::Hdr => "image/vnd.radiance",
Self::Avif => "image/avif",
Self::Fits => "image/fits",
Self::Dpx => "image/dpx",
Self::Jxl => "image/jxl",
Self::Cineon => "image/x-cineon",
Self::Dicom => "application/dicom",
Self::Iff => "image/ilbm",
Self::Ase => "application/x-adobe-swatch-exchange",
Self::Psd => "image/vnd.adobe.photoshop",
Self::Affinity => "application/x-affinity",
Self::Ora => "image/openraster",
Self::Kra => "application/x-krita",
Self::Xcf => "image/x-xcf",
Self::Pdn => "application/x-paint.net",
Self::Raw => "image/x-camera-raw",
Self::Pcraft => "application/vnd.photocraft+zip",
Self::Idml => "application/vnd.adobe.indesign-idml-package",
Self::DesignCraft => "application/vnd.designcraft+zip",
Self::DesignCraftJson => "application/json",
Self::Indd => "application/x-indesign",
Self::Docx => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
Self::LegacyDoc => "application/msword",
Self::Rtf => "application/rtf",
Self::Xlsx => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
Self::TaggedText => "text/plain",
Self::Pptx => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
Self::PlainText => "text/plain",
Self::Csv => "text/csv",
Self::Tsv => "text/tab-separated-values",
Self::LegacyExcel => "application/vnd.ms-excel",
Self::Studio => "application/x-silhouette-studio",
Self::StencilCraft => "application/vnd.stencilcraft+json",
Self::Gcode => "text/x.gcode",
})
}
/// The corresponding flat-raster codec, when this format is handled by `craft-image-codecs`.
#[cfg(feature = "raster-codecs")]
pub fn raster_codec(self) -> Option<craft_image_codecs::Format> {
use craft_image_codecs::Format as Codec;
Some(match self {
Self::Png => Codec::Png,
Self::Jpeg => Codec::Jpeg,
Self::Bmp => Codec::Bmp,
Self::Gif => Codec::Gif,
Self::Tiff => Codec::Tiff,
Self::WebP => Codec::WebP,
Self::Heif => Codec::Heif,
Self::Tga => Codec::Tga,
Self::Ico => Codec::Ico,
Self::Pnm => Codec::Pnm,
Self::Pcx => Codec::Pcx,
Self::Xpm => Codec::Xpm,
Self::Dds => Codec::Dds,
Self::Qoi => Codec::Qoi,
Self::OpenExr => Codec::OpenExr,
Self::Hdr => Codec::Hdr,
Self::Avif => Codec::Avif,
Self::Fits => Codec::Fits,
Self::Dpx => Codec::Dpx,
Self::Jxl => Codec::Jxl,
Self::Cineon => Codec::Cineon,
Self::Dicom => Codec::Dicom,
Self::Iff => Codec::Iff,
_ => return None,
})
}
/// Operations exposed by this build's shared parser/codec features.
pub fn capabilities(self) -> Capabilities {
#[cfg(feature = "raster-codecs")]
if let Some(format) = self.raster_codec() {
let codec = format.caps();
let read = if codec.read {
Availability::Available
} else if matches!(self, Self::Heif) && !cfg!(feature = "heif") {
Availability::BuildDisabled
} else {
Availability::Unsupported
};
let write = if codec.write {
Availability::Available
} else if matches!(self, Self::Avif) && !cfg!(feature = "avif") {
Availability::BuildDisabled
} else {
Availability::Unsupported
};
return Capabilities { read, write, multiple_frames: codec.animation, file_sequence: false };
}
let raster = cfg!(feature = "raster-codecs");
match self {
Self::Png | Self::Jpeg | Self::Bmp | Self::Gif | Self::Tiff | Self::WebP => {
capability(raster, true, true, matches!(self, Self::Png | Self::Gif | Self::WebP), false)
}
Self::Heif => Capabilities {
read: availability(cfg!(feature = "heif"), true),
write: Availability::Unsupported,
multiple_frames: true,
file_sequence: false,
},
Self::Avif => Capabilities {
read: Availability::Unsupported,
write: availability(cfg!(feature = "avif"), true),
multiple_frames: false,
file_sequence: false,
},
Self::Jxl => capability(raster, false, true, false, false),
Self::Pcx | Self::Xpm | Self::Dicom => capability(raster, true, false, false, false),
Self::Tga
| Self::Ico
| Self::Pnm
| Self::Dds
| Self::Qoi
| Self::OpenExr
| Self::Hdr
| Self::Fits
| Self::Dpx
| Self::Cineon
| Self::Iff => capability(raster, true, true, false, false),
Self::Pdf => capability(cfg!(feature = "pdf"), true, true, false, false),
Self::Eps | Self::PostScript | Self::Illustrator => capability(cfg!(feature = "pdf"), true, false, false, false),
Self::Svg | Self::Svgz => capability(cfg!(feature = "svg"), true, false, false, false),
Self::Dxf | Self::Gcode => capability(cfg!(feature = "plotter"), true, false, false, false),
Self::Ora => capability(cfg!(feature = "ora"), true, true, false, false),
Self::Kra => capability(cfg!(feature = "kra"), true, false, false, false),
Self::Xcf => capability(cfg!(feature = "xcf"), true, false, false, false),
Self::Pdn => capability(cfg!(feature = "pdn"), true, false, false, false),
Self::Raw => capability(cfg!(feature = "raw"), true, false, true, true),
Self::Psd => capability(cfg!(feature = "psd"), true, true, false, false),
Self::Affinity => capability(cfg!(feature = "affinity"), true, false, false, false),
Self::Pcraft => capability(cfg!(feature = "pcraft"), true, true, false, false),
Self::Ase => capability(cfg!(feature = "ase"), true, true, false, false),
Self::Studio => capability(cfg!(feature = "stencilcraft"), true, false, false, false),
Self::Idml | Self::Indd => capability(cfg!(feature = "designcraft"), true, false, false, false),
Self::DesignCraft | Self::DesignCraftJson => capability(cfg!(feature = "designcraft"), true, true, false, false),
Self::StencilCraft => capability(cfg!(feature = "stencilcraft"), true, true, false, false),
Self::Docx
| Self::LegacyDoc
| Self::Rtf
| Self::Xlsx
| Self::Pptx
| Self::TaggedText
| Self::PlainText
| Self::Csv
| Self::Tsv
| Self::LegacyExcel => capability(cfg!(feature = "office"), true, true, false, false),
}
}
}
/// Resolve a name and file bytes to a format. Strong signatures take precedence over
/// extension hints. Only format-specific bounded probes and extension tables live here.
pub fn detect(name: &str, bytes: &[u8]) -> Option<Format> {
@@ -183,49 +542,8 @@ fn is_designcraft_json(bytes: &[u8]) -> bool {
/// Resolve a file extension to a family, case-insensitively.
pub fn from_extension(ext: &str) -> Option<Format> {
match ext.trim_start_matches('.').to_ascii_lowercase().as_str() {
"pdf" => Some(Format::Pdf),
"eps" => Some(Format::Eps),
"ps" => Some(Format::PostScript),
"ai" => Some(Format::Illustrator),
"dxf" => Some(Format::Dxf),
"svg" => Some(Format::Svg),
"svgz" => Some(Format::Svgz),
"png" => Some(Format::Png),
"jpg" | "jpeg" => Some(Format::Jpeg),
"bmp" => Some(Format::Bmp),
"gif" => Some(Format::Gif),
"tif" | "tiff" => Some(Format::Tiff),
"webp" => Some(Format::WebP),
"heic" | "heif" => Some(Format::Heif),
"ase" => Some(Format::Ase),
"psd" | "psb" => Some(Format::Psd),
"afdesign" | "afphoto" | "afpub" => Some(Format::Affinity),
"ora" => Some(Format::Ora),
"kra" => Some(Format::Kra),
"xcf" => Some(Format::Xcf),
"pdn" => Some(Format::Pdn),
"dng" | "3fr" | "ari" | "arw" | "bay" | "cap" | "cr2" | "cr3" | "dcs" | "dcr" | "drf" | "eip" | "erf" | "fff" | "gpr" | "iiq" | "k25"
| "kdc" | "mdc" | "mef" | "mos" | "mrw" | "nef" | "nrw" | "obm" | "orf" | "pef" | "ptx" | "pxn" | "r3d" | "raf" | "raw" | "rwl" | "rw2"
| "rwz" | "sr2" | "srf" | "sti" | "x3f" => Some(Format::Raw),
"pcraft" => Some(Format::Pcraft),
"idml" => Some(Format::Idml),
"designcraft" | "dcdoc" => Some(Format::DesignCraft),
"indd" | "indt" => Some(Format::Indd),
"docx" => Some(Format::Docx),
"doc" => Some(Format::LegacyDoc),
"rtf" => Some(Format::Rtf),
"xlsx" => Some(Format::Xlsx),
"pptx" => Some(Format::Pptx),
"txt" | "text" | "md" => Some(Format::PlainText),
"csv" => Some(Format::Csv),
"tsv" | "tab" => Some(Format::Tsv),
"xls" | "xlsm" => Some(Format::LegacyExcel),
"studio" | "studio3" => Some(Format::Studio),
"stencilcraft" => Some(Format::StencilCraft),
"gcode" | "nc" => Some(Format::Gcode),
_ => None,
}
let ext = ext.trim_start_matches('.');
Format::ALL.into_iter().find(|format| format.extensions().iter().any(|candidate| candidate.eq_ignore_ascii_case(ext)))
}
const INDD_SIGNATURE: [u8; 16] = [0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D];