mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Route file dispatch through shared format APIs
This commit is contained in:
15 files changed
+144
-170
No files matched your search
Generated
+60
-14
@@ -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
@@ -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).
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}")));
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()))
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 }
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user