mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
Add shared file format detection
This commit is contained in:
4 files changed
+115
No files matched your search
Generated
+7
@@ -1454,6 +1454,13 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-file-formats"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"craft-eps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-flags"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -42,6 +42,7 @@ craft-docx = { path = "crates/docx" }
|
||||
craft-dxf = { path = "crates/dxf" }
|
||||
craft-drawingml = { path = "crates/drawingml" }
|
||||
craft-eps = { path = "crates/eps" }
|
||||
craft-file-formats = { path = "crates/file-formats" }
|
||||
craft-doc = { path = "crates/doc" }
|
||||
craft-docpath = { path = "crates/docpath" }
|
||||
craft-dpx = { path = "crates/dpx" }
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "craft-file-formats"
|
||||
description = "Shared bounded file signature and extension resolution for Craft applications"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-eps = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Bounded file kind detection and extension resolution shared by Craft applications.
|
||||
//!
|
||||
//! This crate identifies a format; it does not parse, validate, or decode the file.
|
||||
//! Callers remain responsible for choosing which identified formats their product supports.
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
/// A supported file family, independent of the application model that consumes it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Format {
|
||||
Pdf,
|
||||
Eps,
|
||||
PostScript,
|
||||
Illustrator,
|
||||
Dxf,
|
||||
Svg,
|
||||
Raster,
|
||||
Studio,
|
||||
StencilCraft,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
if let Some(found) = craft_eps::sniff(bytes) {
|
||||
return Some(match (found.kind, found.illustrator) {
|
||||
(_, true) => Format::Illustrator,
|
||||
(craft_eps::Kind::Pdf, _) => Format::Pdf,
|
||||
(craft_eps::Kind::Eps, _) => Format::Eps,
|
||||
(craft_eps::Kind::PostScript, _) => Format::PostScript,
|
||||
});
|
||||
}
|
||||
if raster_signature(bytes) {
|
||||
return Some(Format::Raster);
|
||||
}
|
||||
if bytes.starts_with(b"silhouette04;") || bytes.starts_with(b"silhouette05;") {
|
||||
return Some(Format::Studio);
|
||||
}
|
||||
if let Some(ext) = extension(name) {
|
||||
if let Some(format) = from_extension(ext) {
|
||||
return Some(format);
|
||||
}
|
||||
}
|
||||
let head = bytes.get(..4096).unwrap_or(bytes);
|
||||
let xml = head.trim_ascii_start();
|
||||
if xml.starts_with(b"<svg") || (xml.starts_with(b"<?xml") && xml.windows(4).any(|w| w == b"<svg")) {
|
||||
return Some(Format::Svg);
|
||||
}
|
||||
if dxf_ascii_prefix(head) || head.starts_with(b"AutoCAD Binary DXF\r\n\x1a\0") {
|
||||
return Some(Format::Dxf);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 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" | "svgz" => Some(Format::Svg),
|
||||
"png" | "jpg" | "jpeg" | "bmp" | "gif" | "tif" | "tiff" | "webp" | "heic" | "heif" => Some(Format::Raster),
|
||||
"studio" | "studio3" => Some(Format::Studio),
|
||||
"stencilcraft" => Some(Format::StencilCraft),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extension(name: &str) -> Option<&str> {
|
||||
Some(name.rsplit(['/', '\\']).next()?.rsplit_once('.')?.1)
|
||||
}
|
||||
|
||||
fn dxf_ascii_prefix(bytes: &[u8]) -> bool {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
let mut lines = text.lines().map(str::trim);
|
||||
matches!((lines.next(), lines.next(), lines.next(), lines.next()), (Some("0"), Some("SECTION"), Some("2"), Some(section)) if matches!(section, "HEADER" | "CLASSES" | "TABLES" | "BLOCKS" | "ENTITIES" | "OBJECTS"))
|
||||
}
|
||||
|
||||
fn raster_signature(bytes: &[u8]) -> bool {
|
||||
let head = bytes.get(..16).unwrap_or(bytes);
|
||||
head.starts_with(b"\x89PNG\r\n\x1a\n")
|
||||
|| head.starts_with(b"\xff\xd8\xff")
|
||||
|| head.starts_with(b"BM")
|
||||
|| head.starts_with(b"GIF87a")
|
||||
|| head.starts_with(b"GIF89a")
|
||||
|| head.starts_with(b"II*\0")
|
||||
|| head.starts_with(b"MM\0*")
|
||||
|| head.starts_with(b"II+\0")
|
||||
|| head.starts_with(b"MM\0+")
|
||||
|| (head.starts_with(b"RIFF") && head.get(8..12) == Some(b"WEBP"))
|
||||
}
|
||||
Reference in new issue
Block a user