diff --git a/Cargo.lock b/Cargo.lock index 9a8abed4c..a8d8480b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/crates/file-formats/Cargo.toml b/crates/file-formats/Cargo.toml index a1a605918..e7321f502 100644 --- a/crates/file-formats/Cargo.toml +++ b/crates/file-formats/Cargo.toml @@ -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 diff --git a/crates/file-formats/src/lib.rs b/crates/file-formats/src/lib.rs index 7859bf539..d10c22b46 100644 --- a/crates/file-formats/src/lib.rs +++ b/crates/file-formats/src/lib.rs @@ -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 { + 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 { @@ -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 { - 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];