//! IDML (InDesign Markup Language) interchange: [`export_idml`] and [`import_idml`]. //! //! Implemented clean-room from the public IDML File Format Specification and from inspecting //! IDML packages exported from synthetic documents. An IDML package is a ZIP whose first entry is //! an uncompressed `mimetype`, followed by `designmap.xml` (the root `Document` element that //! includes the other parts) and the `Resources/`, `MasterSpreads/`, `Spreads/` and `Stories/` //! parts. //! //! ## Coordinates //! DesignCraft spreads have their origin at the top-left of the first page (y down). IDML spread //! coordinates have their origin at the binding spine (facing pages) or the spread centre //! (single-sided documents), vertically centred on the pages; pages are placed by their //! `ItemTransform` and every page item's geometry is `PathGeometry` in inner coordinates mapped by //! `ItemTransform` (inner → parent). Export maps `xf_idml = T(-origin) · xf`; import computes the //! page rectangles from the page transforms and maps back. //! //! ## Supported subset //! Swatches (process/spot CMYK, RGB and Lab colours, tints, linear/radial gradients), paragraph, //! character and object styles (with `BasedOn` and style groups), fonts, document preferences //! (page size, facing pages, bleed/slug, margins and columns, units, grids), layers, sections, //! parent (master) spreads and applied parents, spreads and pages, page items (`TextFrame`, //! `Rectangle`, `Oval`, `Polygon`, `GraphicLine`, `Group`) with fill/stroke, corner options, text //! frame options (with column rules), text wrap, transparency (opacity, blend mode, drop shadow, //! feather), images (embedded `Contents` or linked), stories with paragraph/character style ranges and local overrides, special //! characters and breaks, and text threading. //! //! ## TODO (ignored on import / not written on export) //! - Tables, footnotes, anchored/inline objects, notes, tracked changes, hyperlinks, index and //! cross-reference markers, text variables, conditions, XML structure (`XML/*` parts). //! - Guides, page-item overrides of parent items (only the page's `OverrideList` is read), //! parent-of-parent page mapping beyond `AppliedMaster`, alternate layouts, liquid layout. //! - Nested/GREP/line styles, bullets & numbering details (bullet glyph, number format), OpenType //! feature flags, paragraph borders, grid alignment, custom dashed stroke styles; column rule //! stroke type and overprint. //! - Mixed inks, gradient stop opacity and gradient feathers, effects other than drop shadow and //! basic feather, EPS/PDF/AI placed graphics (imported as images when the data is available), //! clipping paths, compound-path fill rules. //! - Unknown elements are ignored; nothing is preserved opaquely for round-trip yet. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod arabic; mod cjk; mod export; mod import; mod names; mod xml; #[cfg(test)] mod tests; pub use export::{ExportOptions, export_idml, export_idml_with}; pub use import::{Imported, import_idml, import_idml_report, import_idml_with}; /// The IDML package mimetype (content of the first, stored `mimetype` entry). pub const MIMETYPE: &str = "application/vnd.adobe.indesign-idml-package"; /// The DOM version written to every part. pub const DOM_VERSION: &str = "16.0"; #[derive(Debug, thiserror::Error)] pub enum IdmlError { #[error("not an IDML package: {0}")] NotIdml(String), #[error("IDML part {part}: {msg}")] Part { part: String, msg: String }, #[error("invalid IDML document: {0}")] Invalid(String), } pub type Result = std::result::Result; /// Does `bytes` look like an IDML package (a ZIP whose first, stored entry is the IDML `mimetype`)? pub fn is_idml(bytes: &[u8]) -> bool { bytes.starts_with(b"PK\x03\x04") && bytes.get(30..38) == Some(b"mimetype") && { let extra = bytes.get(28..30).map_or(0, |e| u16::from_le_bytes([e[0], e[1]]) as usize); bytes.get(38 + extra..).is_some_and(|rest| rest.starts_with(MIMETYPE.as_bytes())) } } const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /// Base64 with line breaks every 76 characters (as embedded image `Contents` are written). pub(crate) fn base64_encode(data: &[u8]) -> String { let mut out = String::with_capacity(data.len().div_ceil(3) * 4 + data.len() / 57); for (i, c) in data.chunks(3).enumerate() { if i > 0 && i % 19 == 0 { out.push('\n'); } let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32; out.push(B64[(n >> 18) as usize & 63] as char); out.push(B64[(n >> 12) as usize & 63] as char); out.push(if c.len() > 1 { B64[(n >> 6) as usize & 63] as char } else { '=' }); out.push(if c.len() > 2 { B64[n as usize & 63] as char } else { '=' }); } out } /// Lenient base64 decoding (whitespace and unknown characters are skipped). pub(crate) fn base64_decode(s: &str) -> Vec { let mut out = Vec::with_capacity(s.len() * 3 / 4); let mut buf = 0u32; let mut bits = 0; for b in s.bytes() { let v = match b { b'A'..=b'Z' => b - b'A', b'a'..=b'z' => b - b'a' + 26, b'0'..=b'9' => b - b'0' + 52, b'+' | b'-' => 62, b'/' | b'_' => 63, _ => continue, } as u32; buf = buf << 6 | v; bits += 6; if bits >= 8 { bits -= 8; out.push((buf >> bits) as u8); } } out } /// Sniff an image's MIME type and pixel size from its header (PNG, JPEG, GIF, WebP, TIFF). pub(crate) fn sniff_image(b: &[u8]) -> (Option<&'static str>, Option<(u32, u32)>) { let be32 = |i: usize| b.get(i..i + 4).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]])); if b.starts_with(b"\x89PNG\r\n\x1a\n") { return (Some("image/png"), be32(16).zip(be32(20))); } if b.starts_with(&[0xFF, 0xD8]) { let mut i = 2; while i + 9 < b.len() { if b[i] != 0xFF { i += 1; continue; } let m = b[i + 1]; let len = u16::from_be_bytes([b[i + 2], b[i + 3]]) as usize; if matches!(m, 0xC0..=0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF) { let h = u16::from_be_bytes([b[i + 5], b[i + 6]]) as u32; let w = u16::from_be_bytes([b[i + 7], b[i + 8]]) as u32; return (Some("image/jpeg"), Some((w, h))); } i += 2 + len; } return (Some("image/jpeg"), None); } if b.starts_with(b"GIF8") && b.len() >= 10 { return (Some("image/gif"), Some((u16::from_le_bytes([b[6], b[7]]) as u32, u16::from_le_bytes([b[8], b[9]]) as u32))); } if b.starts_with(b"RIFF") && b.get(8..12) == Some(b"WEBP") { return (Some("image/webp"), None); } if b.starts_with(b"II*\0") || b.starts_with(b"MM\0*") { return (Some("image/tiff"), None); } if b.starts_with(b"%PDF") { return (Some("application/pdf"), None); } (None, None) }