Route file dispatch through shared format APIs

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 01:34:50 -05:00
1 parent fee641be68
commit aff7435b81
15 files changed
+144 -170

No files matched your search

Generated
+60 -14
View File
@@ -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",
+2 -7
View File
@@ -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).
+2 -1
View File
@@ -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 }
+1 -1
View File
@@ -57,7 +57,7 @@ fn load(path: &str) -> Result<designcraft_doc::Document> {
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}")));
}
+16 -5
View File
@@ -84,12 +84,17 @@ fn open_path(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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)?;
+8 -8
View File
@@ -32,9 +32,9 @@ pub(super) fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
})
.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<Value> {
// 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<Value> {
/// 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<Value> {
fn place_layout_page(s: &mut Session, p: &Value, bytes: &[u8], format: Option<craft_file_formats::Format>) -> Result<Value> {
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)?
+3 -3
View File
@@ -89,11 +89,11 @@ pub(super) fn file_revert(s: &mut Session, _: &Value) -> Result<Value> {
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);
+10 -10
View File
@@ -418,7 +418,7 @@ pub(crate) fn open_idml(s: &mut Session, p: &Value) -> Result<Value> {
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();
+1 -1
View File
@@ -85,7 +85,7 @@ fn source(p: &Value, id: &str) -> Result<Document> {
(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()))
+1
View File
@@ -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"] }
-80
View File
@@ -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<ZCursor<&[u8]>> {
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<Vec<u8>>)> {
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
}
+20 -22
View File
@@ -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::Format> {
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",
}
}
+1 -1
View File
@@ -180,7 +180,7 @@ pub(super) fn recompress(data: &[u8], mode: RasterMode) -> Option<Image> {
}
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).
+1
View File
@@ -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 }
+18 -17
View File
@@ -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<Imported, ImportError> {
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)))
}