diff --git a/Cargo.lock b/Cargo.lock index 36d06c8b9..b46aa6550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,7 +1122,7 @@ dependencies = [ "craft-offcrypto", "craft-opc", "craft-wordmodel", - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "log", "quick-xml 0.41.0", "thiserror 2.0.21", @@ -1135,12 +1135,17 @@ name = "craft-drawingml" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "kurbo", "log", "serde", ] +[[package]] +name = "craft-eps" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d#6ba1ca33ca21c33de55ef155141db08ca6048e8d" + [[package]] name = "craft-eps" version = "0.1.0" @@ -1151,11 +1156,20 @@ name = "craft-epub" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "craft-zip", "thiserror 2.0.21", ] +[[package]] +name = "craft-file-formats" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d#6ba1ca33ca21c33de55ef155141db08ca6048e8d" +dependencies = [ + "craft-eps 0.1.0 (git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d)", + "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d)", +] + [[package]] name = "craft-fonts-build" version = "0.1.0" @@ -1189,7 +1203,7 @@ name = "craft-idml" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "craft-zip", "thiserror 2.0.21", ] @@ -1216,6 +1230,14 @@ dependencies = [ "zune-jpegxl", ] +[[package]] +name = "craft-indd" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=47bf5650b0d503b97bfb5e841746c4957897d11f#47bf5650b0d503b97bfb5e841746c4957897d11f" +dependencies = [ + "indd", +] + [[package]] name = "craft-jpeg" version = "0.1.0" @@ -1241,7 +1263,7 @@ name = "craft-math" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "serde", ] @@ -1253,7 +1275,7 @@ dependencies = [ "aes", "base64 0.23.1", "craft-cfb", - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "getrandom 0.3.4", "hmac", "sha1 0.11.0", @@ -1267,7 +1289,7 @@ name = "craft-opc" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "craft-zip", "log", "thiserror 2.0.21", @@ -1337,12 +1359,24 @@ name = "craft-sheet" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" +[[package]] +name = "craft-svg" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d#6ba1ca33ca21c33de55ef155141db08ca6048e8d" +dependencies = [ + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d)", + "flate2", + "kurbo", + "roxmltree 0.21.2", + "thiserror 2.0.21", +] + [[package]] name = "craft-svg" version = "0.1.0" source = "git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf#ba24fe2435adb3c81ad6057faf36365fbba736cf" dependencies = [ - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "flate2", "kurbo", "roxmltree 0.21.2", @@ -1438,7 +1472,16 @@ dependencies = [ "craft-drawingml", "craft-opc", "craft-sheet", - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "thiserror 2.0.21", +] + +[[package]] +name = "craft-xml" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=6ba1ca33ca21c33de55ef155141db08ca6048e8d#6ba1ca33ca21c33de55ef155141db08ca6048e8d" +dependencies = [ + "quick-xml 0.41.0", "thiserror 2.0.21", ] @@ -1697,6 +1740,8 @@ dependencies = [ "caseless", "craft-atomic", "craft-docpath", + "craft-file-formats", + "craft-indd", "craft-prepress", "craft-print", "designcraft-color", @@ -1716,7 +1761,6 @@ dependencies = [ "hayro-cmap", "hayro-interpret", "hayro-syntax", - "indd", "kurbo", "log", "quick-xml 0.41.0", @@ -1758,7 +1802,7 @@ name = "designcraft-format" version = "0.20.1" dependencies = [ "craft-document-bundle", - "craft-eps", + "craft-eps 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "designcraft-doc", "serde", "serde_json", @@ -1784,7 +1828,7 @@ name = "designcraft-idml" version = "0.20.1" dependencies = [ "craft-idml", - "craft-xml", + "craft-xml 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -1800,10 +1844,11 @@ name = "designcraft-images" version = "0.20.1" dependencies = [ "craft-cms", - "craft-eps", + "craft-eps 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", + "craft-file-formats", "craft-image-codecs", "craft-psd", - "craft-svg", + "craft-svg 0.1.0 (git+https://github.com/storytold/craft-libs?rev=ba24fe2435adb3c81ad6057faf36365fbba736cf)", "designcraft-color", "designcraft-fonts", "flate2", @@ -1896,6 +1941,7 @@ name = "designcraft-textimport" version = "0.20.1" dependencies = [ "craft-docx", + "craft-file-formats", "craft-idml", "craft-rtf", "craft-wordmodel", diff --git a/Cargo.toml b/Cargo.toml index 54833bb50..2b2f09020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ craft-document-bundle = { git = "https://github.com/storytold/craft-libs", rev = craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-epub = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-idml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } +craft-indd = { git = "https://github.com/storytold/craft-libs", rev = "47bf5650b0d503b97bfb5e841746c4957897d11f", version = "=0.1.0" } craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } @@ -25,6 +26,7 @@ craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe243 craft-wordmodel = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-image-codecs = { git = "https://github.com/storytold/craft-libs", rev = "a47b78256c6acf3d7fdc4bc7cfbc25c373eac522", version = "=0.1.0" } craft-psd = { git = "https://github.com/storytold/craft-libs", rev = "a47b78256c6acf3d7fdc4bc7cfbc25c373eac522", version = "=0.1.0" } +craft-file-formats = { git = "https://github.com/storytold/craft-libs", rev = "6ba1ca33ca21c33de55ef155141db08ca6048e8d", version = "=0.1.0" } # Shared keyboard bindings: chords, overrides, conflicts, egui dispatch, the Keyboard Shortcuts dialog. craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", features = ["egui", "ui"] } # Separations, ink coverage and the ink manager; same revision as craft-pdf and the krilla patch. @@ -88,13 +90,6 @@ flo_curves = "0.8" qrcodegen = "1.8" quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } -# INDD/INDT → IDML conversion for File › Open: storytold/indd-utils, pinned by rev as every other -# git dependency here is. It parses untrusted INDD files, and `branch = "main"` meant any -# `cargo update` -- Dependabot runs them -- pulled whatever was on that branch into the parser -# without review (#1599). -# (MIT OR Apache-2.0, no dependencies, builds for wasm). -indd = { git = "https://github.com/storytold/indd-utils", rev = "7f3060e5bce78d21ea70e0d5eacb96edce15dfbe" } - # Which paths stored in documents may be followed (craft-libs). craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } # Atomic file replacement for saves and exports (craft-libs). diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index c1c0f1d33..ed9e7e3bc 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -26,7 +26,8 @@ designcraft-pdf = { workspace = true } craft-prepress = { workspace = true } designcraft-tools = { workspace = true } designcraft-idml = { workspace = true } -indd = { workspace = true } +craft-file-formats = { workspace = true } +craft-indd = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/crates/engine/src/cmd/book.rs b/crates/engine/src/cmd/book.rs index a4217f825..b1de3dac7 100644 --- a/crates/engine/src/cmd/book.rs +++ b/crates/engine/src/cmd/book.rs @@ -57,7 +57,7 @@ fn load(path: &str) -> Result { let _ = path; vec![] }; - if path.to_lowercase().ends_with(".idml") { + if craft_file_formats::detect(path, &bytes) == Some(craft_file_formats::Format::Idml) { return super::interchange::import_idml_confined(&bytes, Some(std::path::Path::new(path))) .map_err(|e| EngineError::Other(format!("{path}: {e}"))); } diff --git a/crates/engine/src/cmd/file/open.rs b/crates/engine/src/cmd/file/open.rs index 529ec2d96..e88d75afc 100644 --- a/crates/engine/src/cmd/file/open.rs +++ b/crates/engine/src/cmd/file/open.rs @@ -84,12 +84,17 @@ fn open_path(s: &mut Session, p: &Value) -> Result { let bytes = crate::io::read_file(path).map_err(EngineError::Other)?; // The content decides first: a file's extension may be missing or wrong. The extension // then routes files whose content isn't recognised to the reader that explains why. - let ext = std::path::Path::new(path).extension().and_then(std::ffi::OsStr::to_str).unwrap_or_default().to_ascii_lowercase(); - let native = designcraft_format::is_designcraft(&bytes) || bytes.trim_ascii_start().starts_with(b"{"); - if designcraft_idml::is_idml(&bytes) || (!native && (crate::cmd::interchange::is_indd(path, &bytes) || ext == "idml")) { + let format = craft_file_formats::detect(path, &bytes); + let native = format == Some(craft_file_formats::Format::DesignCraft) + || designcraft_format::is_designcraft(&bytes) + || bytes.trim_ascii_start().starts_with(b"{"); + if format == Some(craft_file_formats::Format::Idml) + || designcraft_idml::is_idml(&bytes) + || (!native && format == Some(craft_file_formats::Format::Indd)) + { return crate::cmd::interchange::open_idml(s, p); } - if !native && ext != designcraft_format::EXTENSION { + if !native { return Err(bad("file.open", format!("{path}: not a DesignCraft or IDML file"))); } let (mut d, mut warnings) = load_bytes(&bytes)?; @@ -156,7 +161,13 @@ pub(super) fn file_open_bytes(s: &mut Session, p: &Value) -> Result { let b = base64_decode(str_param(p, "base64").unwrap_or("")); // IDML packages are recognised by their stored `mimetype` first entry, INDD files by their // signature (or their name). - if designcraft_idml::is_idml(&b) || crate::cmd::interchange::is_indd(str_param(p, "name").unwrap_or_default(), &b) { + let name = str_param(p, "name").unwrap_or_default(); + let format = craft_file_formats::detect(name, &b); + let native = designcraft_format::is_designcraft(&b) || b.trim_ascii_start().starts_with(b"{"); + if format == Some(craft_file_formats::Format::Idml) + || designcraft_idml::is_idml(&b) + || (!native && format == Some(craft_file_formats::Format::Indd)) + { return crate::cmd::interchange::open_idml(s, p); } let (mut d, warnings) = load_bytes(&b)?; diff --git a/crates/engine/src/cmd/file/place.rs b/crates/engine/src/cmd/file/place.rs index a30cb2321..6d5236734 100644 --- a/crates/engine/src/cmd/file/place.rs +++ b/crates/engine/src/cmd/file/place.rs @@ -32,9 +32,9 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { return Err(bad("file.place", "the frame is locked")); } // A page of another layout (IDML or DesignCraft): its objects, as one group. - let lname = name.to_lowercase(); - if lname.ends_with(".idml") || lname.ends_with(".designcraft") { - return place_layout_page(s, p, &name, &bytes); + let format = craft_file_formats::detect(&name, &bytes); + if matches!(format, Some(craft_file_formats::Format::Idml | craft_file_formats::Format::DesignCraft)) { + return place_layout_page(s, p, &bytes, format); } // Keep the EPS source for link comparison; rendering uses its preview at the bounding box size. let eps_size = eps_size(&bytes, "file.place")?; @@ -57,11 +57,11 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { }) .transpose()?; // Image Import Options: which page of a PDF (1-based). - if designcraft_render::is_pdf(&bytes) { + if format == Some(craft_file_formats::Format::Pdf) { designcraft_render::check_pdf_input_size(bytes.len()).map_err(|e| bad("file.place", e.to_string()))?; } let pdf_page = match p.get("pdfPage").and_then(Value::as_u64) { - Some(n) if designcraft_render::is_pdf(&bytes) => { + Some(n) if format == Some(craft_file_formats::Format::Pdf) => { let count = designcraft_render::pdf_page_count(&bytes).unwrap_or(1); let invalid_page = || bad("file.place", format!("the PDF has {count} page(s)")); let one_based = usize::try_from(n).map_err(|_| invalid_page())?; @@ -83,7 +83,7 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result { // and SVGs keep their exact size: rounding it to whole points would stretch them unevenly. let (nw, nh) = eps_size.or_else(|| designcraft_render::natural_size(&bytes, pdf_page as usize)).unwrap_or((pw as f64, ph as f64)); // Image Import Options › Crop to: the graphic spans that box of the PDF page. - let is_pdf = designcraft_render::is_pdf(&bytes); + let is_pdf = format == Some(craft_file_formats::Format::Pdf); let pdf_crop = match str_param(p, "pdfCrop") { Some(k) if is_pdf => PdfCrop::from_name(k) .ok_or_else(|| bad("file.place", format!("unknown pdfCrop `{k}` (crop, art, trim, bleed, media, contentVisible, contentAll)")))?, @@ -347,9 +347,9 @@ pub(super) fn place_drop(s: &mut Session, p: &Value) -> Result { /// File › Place of an IDML or DesignCraft document: the objects of one page (`layoutPage`, /// 1-based) grouped, top-left at x/y (default: the source position on the first page). -fn place_layout_page(s: &mut Session, p: &Value, name: &str, bytes: &[u8]) -> Result { +fn place_layout_page(s: &mut Session, p: &Value, bytes: &[u8], format: Option) -> Result { const ID: &str = "file.place"; - let src = if name.to_lowercase().ends_with(".idml") { + let src = if format == Some(craft_file_formats::Format::Idml) { crate::cmd::interchange::import_idml_confined(bytes, str_param(p, "path").map(std::path::Path::new)).map_err(|e| bad(ID, e.to_string()))? } else { from_bytes(bytes)? diff --git a/crates/engine/src/cmd/file/save.rs b/crates/engine/src/cmd/file/save.rs index d94a02ab9..d12bf03fa 100644 --- a/crates/engine/src/cmd/file/save.rs +++ b/crates/engine/src/cmd/file/save.rs @@ -89,11 +89,11 @@ pub(super) fn file_revert(s: &mut Session, _: &Value) -> Result { let path = s.doc()?.path.clone().ok_or_else(|| bad("file.revert", "the document has never been saved"))?; #[cfg(not(target_arch = "wasm32"))] { - let mut d = if path.to_ascii_lowercase().ends_with(".idml") { - let bytes = crate::io::read_file(&path).map_err(EngineError::Other)?; + let bytes = crate::io::read_file(&path).map_err(EngineError::Other)?; + let mut d = if craft_file_formats::detect(&path, &bytes) == Some(craft_file_formats::Format::Idml) { crate::cmd::interchange::import_idml_confined(&bytes, Some(std::path::Path::new(&path))).map_err(|e| EngineError::Other(e.to_string()))? } else { - from_bytes(&crate::io::read_file(&path).map_err(EngineError::Other)?)? + from_bytes(&bytes)? }; // As opened again: the fonts beside it now (unchanged files are shared, not read again). let (fonts, _, _) = load_document_fonts(&mut d, &path); diff --git a/crates/engine/src/cmd/interchange.rs b/crates/engine/src/cmd/interchange.rs index 47b36b776..0fba3b900 100644 --- a/crates/engine/src/cmd/interchange.rs +++ b/crates/engine/src/cmd/interchange.rs @@ -418,7 +418,7 @@ pub(crate) fn open_idml(s: &mut Session, p: &Value) -> Result { let mut previews = std::collections::HashMap::new(); let bytes = if is_indd(&file, &bytes) { // What the converter can't read still comes over as IDML (#21). - let conversion = indd::convert(&bytes, &file).map_err(|e| { + let conversion = craft_indd::convert(&bytes, &file).map_err(|e| { EngineError::Other(format!( "can't convert the InDesign file: {e}. In InDesign, choose File ▸ Export… ▸ InDesign Markup (IDML) and open the .idml in DesignCraft." )) @@ -470,19 +470,19 @@ fn warning_item( /// the bytes are neither a recognized DesignCraft document nor IDML. The converter explains /// unrecognized data with an InDesign extension. pub(crate) fn is_indd(name: &str, bytes: &[u8]) -> bool { - bytes.starts_with(&indd::header::SIGNATURE) - || (has_indd_extension(name) && !super::file::is_document(bytes) && !bytes.trim_ascii_start().starts_with(b"{")) -} - -pub(crate) fn has_indd_extension(name: &str) -> bool { - let n = name.to_ascii_lowercase(); - n.ends_with(".indd") || n.ends_with(".indt") + craft_file_formats::detect(name, bytes) == Some(craft_file_formats::Format::Indd) + && !super::file::is_document(bytes) + && !bytes.trim_ascii_start().starts_with(b"{") } /// `name` without a trailing .idml, .indd or .indt (any case). fn without_document_extension(name: &str) -> &str { match name.rsplit_once('.') { - Some((stem, ext)) if ["idml", "indd", "indt"].iter().any(|e| ext.eq_ignore_ascii_case(e)) => stem, + Some((stem, ext)) + if matches!(craft_file_formats::from_extension(ext), Some(craft_file_formats::Format::Idml | craft_file_formats::Format::Indd)) => + { + stem + } _ => name, } } @@ -683,7 +683,7 @@ mod tests { /// name) go to the converter, and what it can't read is an open error that adds no document. #[test] fn indd_files_go_through_the_converter_and_its_errors_are_open_errors() { - let mut signed = indd::header::SIGNATURE.to_vec(); + let mut signed = craft_indd::SIGNATURE.to_vec(); signed.resize(4096, 0); let cases = [(signed, "Brochure"), (vec![0; 4096], "Brochure.indd"), (b"not an InDesign file".to_vec(), "Template.INDT")]; let mut s = Session::new(); diff --git a/crates/engine/src/cmd/load_styles.rs b/crates/engine/src/cmd/load_styles.rs index 762f7b1fc..154306048 100644 --- a/crates/engine/src/cmd/load_styles.rs +++ b/crates/engine/src/cmd/load_styles.rs @@ -85,7 +85,7 @@ fn source(p: &Value, id: &str) -> Result { (Some(path), None) => (crate::io::read_file(path).map_err(|e| bad(id, e))?, path.to_string()), _ => return Err(bad(id, "give `path` or `base64`")), }; - if name.to_lowercase().ends_with(".idml") { + if craft_file_formats::detect(&name, &bytes) == Some(craft_file_formats::Format::Idml) { designcraft_idml::import_idml(&bytes).map_err(|e| bad(id, e.to_string())) } else { super::file::from_bytes(&bytes).map_err(|e| bad(id, e.to_string())) diff --git a/crates/images/Cargo.toml b/crates/images/Cargo.toml index 6b58daccb..f2cd6b362 100644 --- a/crates/images/Cargo.toml +++ b/crates/images/Cargo.toml @@ -20,6 +20,7 @@ log = { workspace = true } craft-cms = { workspace = true } craft-image-codecs = { workspace = true } craft-psd = { workspace = true } +craft-file-formats = { workspace = true } [dev-dependencies] designcraft-fonts = { workspace = true, features = ["testing"] } diff --git a/crates/images/src/jpeg.rs b/crates/images/src/jpeg.rs deleted file mode 100644 index 8b44d0c9a..000000000 --- a/crates/images/src/jpeg.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! CMYK and YCCK JPEGs, decoded to their ink values (the `image` crate converts them to RGB with -//! a profile-free formula). - -use zune_core::bytestream::ZCursor; -use zune_core::colorspace::ColorSpace; -use zune_core::options::DecoderOptions; -use zune_jpeg::JpegDecoder; - -use crate::CmykRaster; - -/// The largest decoded image, in bytes (the `image` crate's default allocation limit). -const MAX_BYTES: usize = 512 << 20; - -fn decoder(bytes: &[u8], out: ColorSpace) -> JpegDecoder> { - let options = DecoderOptions::default().set_strict_mode(false).set_max_width(65_535).set_max_height(65_535).jpeg_set_out_colorspace(out); - JpegDecoder::new_with_options(ZCursor::new(bytes), options) -} - -/// A four-channel JPEG's inks (0 = no ink) and its embedded ICC profile. `None` for other JPEGs -/// and for anything that doesn't decode. -pub(crate) fn decode_cmyk(bytes: &[u8]) -> Option<(CmykRaster, Option>)> { - if !bytes.starts_with(&[0xFF, 0xD8]) { - return None; - } - let mut d = decoder(bytes, ColorSpace::RGB); - d.decode_headers().ok()?; - let space = d.input_colorspace()?; - if !matches!(space, ColorSpace::CMYK | ColorSpace::YCCK) { - return None; - } - let info = d.info()?; - let (w, h) = (usize::from(info.width), usize::from(info.height)); - let pixels = w.checked_mul(h)?; - if pixels.checked_mul(4)? > MAX_BYTES { - return None; - } - // Decoding into the input colour space copies the channels as stored. - let mut d = decoder(bytes, space); - let mut px = d.decode().ok()?; - if px.len() != pixels.checked_mul(4)? { - return None; - } - let icc = d.icc_profile(); - if space == ColorSpace::YCCK { - // YCbCr encodes 255 − the first three stored values (JFIF conversion); K is as stored. - for p in px.as_chunks_mut::<4>().0 { - let (y, cb, cr) = (f32::from(p[0]), f32::from(p[1]) - 128.0, f32::from(p[2]) - 128.0); - let byte = |v: f32| 255 - v.round().clamp(0.0, 255.0) as u8; - p[0] = byte(y + 1.402 * cr); - p[1] = byte(y - 0.344_136 * cb - 0.714_136 * cr); - p[2] = byte(y + 1.772 * cb); - } - } - // Adobe's APP14 marker (always there for YCCK) means the values are stored inverted: - // 255 = no ink. - if space == ColorSpace::YCCK || adobe_marker(bytes) { - for v in &mut px { - *v = 255 - *v; - } - } - let raster = CmykRaster { width: u32::try_from(w).ok()?, height: u32::try_from(h).ok()?, cmyk: px, alpha: None }; - Some((raster, icc)) -} - -/// Whether an APP14 "Adobe" segment comes before the image data. -fn adobe_marker(bytes: &[u8]) -> bool { - let mut i = 2; - while let Some(&[0xFF, marker, hi, lo]) = bytes.get(i..i + 4).and_then(|s| <&[u8; 4]>::try_from(s).ok()) { - // Start of scan or end of image: no more header segments. - if marker == 0xDA || marker == 0xD9 { - return false; - } - let len = usize::from(u16::from_be_bytes([hi, lo])); - if marker == 0xEE && bytes.get(i + 4..i + 9) == Some(b"Adobe".as_slice()) { - return true; - } - i += 2 + len; - } - false -} diff --git a/crates/images/src/lib.rs b/crates/images/src/lib.rs index b9209e65e..a4c9d9b86 100644 --- a/crates/images/src/lib.rs +++ b/crates/images/src/lib.rs @@ -20,39 +20,37 @@ pub use tone::{Tone, tone}; pub const PT_PER_PX: f64 = 0.75; pub fn is_pdf(bytes: &[u8]) -> bool { - bytes.starts_with(b"%PDF") + file_format(bytes) == Some(craft_file_formats::Format::Pdf) } pub fn is_psd(bytes: &[u8]) -> bool { - craft_psd::head::sniff(bytes) + file_format(bytes) == Some(craft_file_formats::Format::Psd) } /// An SVG document (optionally after an XML declaration, comments or a doctype). pub fn is_svg(bytes: &[u8]) -> bool { - craft_svg::sniff::is_svg(bytes) + file_format(bytes) == Some(craft_file_formats::Format::Svg) +} + +pub fn file_format(bytes: &[u8]) -> Option { + craft_file_formats::detect("", bytes) } /// MIME type of a placed graphic. pub fn mime(bytes: &[u8]) -> &'static str { - if is_eps(bytes) { - return "application/postscript"; - } - if is_pdf(bytes) { - return "application/pdf"; - } - if is_psd(bytes) { - return "image/vnd.adobe.photoshop"; - } - if is_svg(bytes) { - return "image/svg+xml"; - } - match craft_image_codecs::detect(bytes) { - Some(craft_image_codecs::Format::Png) => "image/png", - Some(craft_image_codecs::Format::Jpeg) => "image/jpeg", - Some(craft_image_codecs::Format::Gif) => "image/gif", - Some(craft_image_codecs::Format::WebP) => "image/webp", - Some(craft_image_codecs::Format::Tiff) => "image/tiff", - Some(craft_image_codecs::Format::Bmp) => "image/bmp", + match file_format(bytes) { + Some(craft_file_formats::Format::Eps | craft_file_formats::Format::PostScript | craft_file_formats::Format::Illustrator) => { + "application/postscript" + } + Some(craft_file_formats::Format::Pdf) => "application/pdf", + Some(craft_file_formats::Format::Psd) => "image/vnd.adobe.photoshop", + Some(craft_file_formats::Format::Svg) => "image/svg+xml", + Some(craft_file_formats::Format::Png) => "image/png", + Some(craft_file_formats::Format::Jpeg) => "image/jpeg", + Some(craft_file_formats::Format::Gif) => "image/gif", + Some(craft_file_formats::Format::WebP) => "image/webp", + Some(craft_file_formats::Format::Tiff) => "image/tiff", + Some(craft_file_formats::Format::Bmp) => "image/bmp", _ => "application/octet-stream", } } diff --git a/crates/pdf/src/export/images.rs b/crates/pdf/src/export/images.rs index f4d4a0a98..4ddafa82d 100644 --- a/crates/pdf/src/export/images.rs +++ b/crates/pdf/src/export/images.rs @@ -180,7 +180,7 @@ pub(super) fn recompress(data: &[u8], mode: RasterMode) -> Option { } let img = craft_image_codecs::decode(data).ok()?; let rgba = img.to_rgba8(); - if img.layout().has_alpha() && rgba.chunks_exact(4).any(|p| p[3] < 255) { + if img.layout().has_alpha() && rgba.as_chunks::<4>().0.iter().any(|p| p[3] < 255) { return None; } // A grey image stays a grey JPEG (#1426). diff --git a/crates/textimport/Cargo.toml b/crates/textimport/Cargo.toml index 460458ddf..968a488c4 100644 --- a/crates/textimport/Cargo.toml +++ b/crates/textimport/Cargo.toml @@ -15,6 +15,7 @@ craft-xlsx = { workspace = true } craft-docx = { workspace = true } craft-wordmodel = { workspace = true } craft-idml = { workspace = true } +craft-file-formats = { workspace = true } [dev-dependencies] zip = { workspace = true } diff --git a/crates/textimport/src/lib.rs b/crates/textimport/src/lib.rs index 0838861e2..78a619bbc 100644 --- a/crates/textimport/src/lib.rs +++ b/crates/textimport/src/lib.rs @@ -64,27 +64,28 @@ impl std::fmt::Display for ImportError { /// Text files this crate reads, by extension. pub fn is_text_file(name: &str) -> bool { - let l = name.to_ascii_lowercase(); - l.ends_with(".txt") || l.ends_with(".text") || l.ends_with(".md") || l.ends_with(".docx") || l.ends_with(".rtf") || l.ends_with(".xlsx") + matches!( + craft_file_formats::detect(name, &[]), + Some( + craft_file_formats::Format::PlainText + | craft_file_formats::Format::Docx + | craft_file_formats::Format::Rtf + | craft_file_formats::Format::Xlsx + ) + ) } /// Import `bytes` (format from the file name, else sniffed). pub fn import(name: &str, bytes: &[u8]) -> Result { - let l = name.to_ascii_lowercase(); - if l.ends_with(".xlsx") { - return xlsx::import(bytes); - } - if l.ends_with(".docx") || bytes.starts_with(b"PK\x03\x04") { - return docx::import(bytes); - } - if l.ends_with(".rtf") || bytes.starts_with(b"{\\rtf") { - return rtf::import(bytes); - } - if tagged::is_tagged(bytes) { - return tagged::import(bytes); - } - if l.ends_with(".doc") { - return Err(ImportError::Unsupported("legacy Word .doc (save it as .docx)".into())); + match craft_file_formats::detect(name, bytes) { + Some(craft_file_formats::Format::Xlsx) => return xlsx::import(bytes), + Some(craft_file_formats::Format::Docx) => return docx::import(bytes), + Some(craft_file_formats::Format::Rtf) => return rtf::import(bytes), + Some(craft_file_formats::Format::TaggedText) => return tagged::import(bytes), + Some(craft_file_formats::Format::LegacyDoc) => { + return Err(ImportError::Unsupported("legacy Word .doc (save it as .docx)".into())); + } + _ => {} } Ok(plain(&tagged::decode(bytes))) }