From 32348da0738967d7326f85c77b11883a699bb596 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 01:19:41 -0500 Subject: [PATCH] Route PDF parsing and raster import through craft-libs --- Cargo.lock | 8 +- Cargo.toml | 4 +- crates/pdf/Cargo.toml | 5 +- crates/pdf/src/export.rs | 18 ++- crates/pdf/src/export/graphics.rs | 8 +- crates/pdf/src/export/images.rs | 70 ++++----- crates/pdf/src/optional_content.rs | 8 +- crates/pdf/src/pdfx.rs | 24 +-- crates/pdf/src/pdfx/fonts.rs | 8 +- crates/pdf/src/pdfx/fonts/tests.rs | 2 +- crates/pdf/src/pdfx/tests.rs | 14 +- crates/pdf/src/pdfx1a.rs | 18 +-- crates/pdf/src/print_tests.rs | 6 +- crates/pdf/src/registration_tests.rs | 14 +- crates/pdf/src/rtl_tests.rs | 16 +- crates/pdf/src/tests.rs | 25 ++-- crates/pdf/src/x1a_tests.rs | 10 +- crates/pdf/tests/it/gradient_color_spaces.rs | 8 +- crates/pdf/tests/it/spot_gradients.rs | 8 +- .../support/image_content_cache_contract.rs | 6 +- crates/textimport/src/docx.rs | 137 ++++++++---------- 21 files changed, 207 insertions(+), 210 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e046d2799..36d06c8b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,7 +1197,7 @@ dependencies = [ [[package]] name = "craft-image-codecs" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=85145d8dfd52966eacd045683a86a3737883aeea#85145d8dfd52966eacd045683a86a3737883aeea" +source = "git+https://github.com/storytold/craft-libs?rev=a47b78256c6acf3d7fdc4bc7cfbc25c373eac522#a47b78256c6acf3d7fdc4bc7cfbc25c373eac522" dependencies = [ "craft-jpeg", "exr", @@ -1219,7 +1219,7 @@ dependencies = [ [[package]] name = "craft-jpeg" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=85145d8dfd52966eacd045683a86a3737883aeea#85145d8dfd52966eacd045683a86a3737883aeea" +source = "git+https://github.com/storytold/craft-libs?rev=a47b78256c6acf3d7fdc4bc7cfbc25c373eac522#a47b78256c6acf3d7fdc4bc7cfbc25c373eac522" dependencies = [ "thiserror 2.0.21", ] @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "craft-psd" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=85145d8dfd52966eacd045683a86a3737883aeea#85145d8dfd52966eacd045683a86a3737883aeea" +source = "git+https://github.com/storytold/craft-libs?rev=a47b78256c6acf3d7fdc4bc7cfbc25c373eac522#a47b78256c6acf3d7fdc4bc7cfbc25c373eac522" dependencies = [ "encoding_rs", "flate2", @@ -1833,6 +1833,7 @@ version = "0.20.1" dependencies = [ "chrono", "craft-cms", + "craft-image-codecs", "craft-pdf", "craft-prepress", "designcraft-color", @@ -1844,7 +1845,6 @@ dependencies = [ "flate2", "hayro-cmap", "hayro-interpret", - "hayro-syntax", "image", "krilla-svg", "kurbo", diff --git a/Cargo.toml b/Cargo.toml index 955a05a94..54833bb50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ craft-rtf = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435 craft-xlsx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } craft-docx = { git = "https://github.com/storytold/craft-libs", rev = "ba24fe2435adb3c81ad6057faf36365fbba736cf", version = "=0.1.0" } 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 = "85145d8dfd52966eacd045683a86a3737883aeea", version = "=0.1.0" } -craft-psd = { git = "https://github.com/storytold/craft-libs", rev = "85145d8dfd52966eacd045683a86a3737883aeea", 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" } # 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. diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 3df877c5c..069c1162d 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -14,7 +14,8 @@ designcraft-color = { workspace = true } designcraft-doc = { workspace = true } designcraft-fonts = { workspace = true } designcraft-compose = { workspace = true } -craft-pdf = { workspace = true, features = ["write-pdf"] } +craft-pdf = { workspace = true, features = ["read", "write-pdf"] } +craft-image-codecs = { workspace = true } craft-prepress = { workspace = true } craft-cms = { workspace = true } krilla-svg = { workspace = true } @@ -23,8 +24,6 @@ image = { workspace = true } thiserror = { workspace = true } log = { workspace = true } chrono = { workspace = true } -# The placed-PDF version check names hayro's PdfVersion (krilla::pdf::Pdf::version returns it). -hayro-syntax = "0.7.2" [dev-dependencies] hayro-interpret = { version = "0.7.0", default-features = false, features = ["embed-fonts"] } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index e4fa19e5c..3d7d53699 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -77,7 +77,7 @@ pub(crate) struct Exporter<'a> { #[cfg(test)] image_conversions: usize, /// Placed PDFs (embedded as vector pages) and their page counts. - pdfs: HashMap<(AssetId, Vec), Option<(krilla::pdf::PdfDocument, usize)>>, + pdfs: HashMap<(AssetId, Vec), Option<(krilla::pdf::PdfDocument, usize)>>, variants: HashMap<(AssetId, Option>), crate::optional_content::Variant>, has_optional_content: bool, #[cfg(test)] @@ -192,16 +192,20 @@ impl GreyImage { if !is_grey(data) { return None; } - let img = image::load_from_memory(data).ok()?.to_luma_alpha8(); - let size = img.dimensions(); + let img = craft_image_codecs::decode(data).ok()?; + if !img.layout().is_gray() { + return None; + } + let img = img.convert(craft_image_codecs::ChannelLayout::GrayA, craft_image_codecs::SampleType::U8); + let size = (img.width(), img.height()); let pixels = usize::try_from(size.0).ok()?.checked_mul(usize::try_from(size.1).ok()?)?; if pixels == 0 { return None; } let (mut grey, mut alpha) = (Vec::with_capacity(pixels), Vec::with_capacity(pixels)); - for p in img.pixels() { - grey.push(p.0[0]); - alpha.push(p.0[1]); + for p in img.data().chunks_exact(2) { + grey.push(p[0]); + alpha.push(p[1]); } let alpha = alpha.iter().any(|a| *a < 255).then(|| Arc::new(alpha)); let icc = embedded_icc(data).filter(|p| is_grey_icc(p)).map(Arc::new); @@ -296,7 +300,7 @@ mod unwind_tests { FAIL_NEXT_SURFACE.set(true); assert!(matches!(export(), Err(PdfError::Write(message)) if message.contains("synthetic export drawing failure")), "{entry}"); let fresh = export().unwrap(); - assert!(!hayro_syntax::Pdf::new(Arc::new(fresh)).unwrap().pages().is_empty(), "fresh {entry} export must still work"); + assert!(!craft_pdf::syntax::Pdf::new(Arc::new(fresh)).unwrap().pages().is_empty(), "fresh {entry} export must still work"); } println!("all three export entry points recovered"); } diff --git a/crates/pdf/src/export/graphics.rs b/crates/pdf/src/export/graphics.rs index 96780de32..aaa31150c 100644 --- a/crates/pdf/src/export/graphics.rs +++ b/crates/pdf/src/export/graphics.rs @@ -12,7 +12,7 @@ impl Exporter<'_> { /// file is left out and the export fails naming it ([`Exporter::check_placed_versions`]). /// Returns the document with its page count. fn placed_pdf(&mut self, asset: &designcraft_doc::Asset, data: Arc>) -> Option<(krilla::pdf::PdfDocument, usize)> { - use hayro_syntax::PdfVersion as V; + use craft_pdf::syntax::PdfVersion as V; let Ok(pdf) = krilla::pdf::Pdf::new(data.clone()) else { self.warn(format!("{}: can't read the placed PDF", asset.name)); return None; @@ -84,7 +84,7 @@ impl Exporter<'_> { self.images.insert(id, Some(converted.image.clone())); return Some(converted.image); } - let fmt = image::guess_format(&data).ok(); + let fmt = craft_image_codecs::detect(&data); #[cfg(test)] { self.image_conversions += 1; @@ -92,10 +92,10 @@ impl Exporter<'_> { let mut warning = None; let mode = RasterMode::for_standard(self.opts.standard); let img = match fmt { - Some(image::ImageFormat::Jpeg) => jpeg(data.clone().into(), || embedded_icc(&data), mode), + Some(craft_image_codecs::Format::Jpeg) => jpeg(data.clone().into(), || embedded_icc(&data), mode), // A CMYK TIFF keeps its ink values (`image` decodes it to RGB, and 100% K would print // as four-colour black). PDF/A exports are RGB only. - Some(image::ImageFormat::Tiff) if !self.rgb_only && designcraft_images::is_cmyk_tiff(&data) => match cmyk_tiff(&data, mode) { + Some(craft_image_codecs::Format::Tiff) if !self.rgb_only && designcraft_images::is_cmyk_tiff(&data) => match cmyk_tiff(&data, mode) { Some(img) => Some(img), None => { warning = Some(CMYK_FALLBACK_WARNING); diff --git a/crates/pdf/src/export/images.rs b/crates/pdf/src/export/images.rs index ec7c76fd6..f4d4a0a98 100644 --- a/crates/pdf/src/export/images.rs +++ b/crates/pdf/src/export/images.rs @@ -46,9 +46,7 @@ pub(super) struct RasterMode { /// The ICC profile embedded in an image file, if it has one. pub(super) fn embedded_icc(data: &[u8]) -> Option> { - use image::ImageDecoder; - let mut dec = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format().ok()?.into_decoder().ok()?; - dec.icc_profile().ok().flatten() + craft_image_codecs::embedded_icc(data).ok().flatten() } /// Whether an ICC profile describes RGB (its colour space signature, header bytes 16 to 20). @@ -63,12 +61,12 @@ pub(super) fn is_grey_icc(icc: &[u8]) -> bool { /// Whether `data` decodes to grey (with or without alpha), read from its header. pub(super) fn is_grey(data: &[u8]) -> bool { - use image::ImageDecoder; - image::ImageReader::new(std::io::Cursor::new(data)) - .with_guessed_format() - .ok() - .and_then(|r| r.into_decoder().ok()) - .is_some_and(|dec| !dec.color_type().has_color()) + craft_image_codecs::inspect_raster(data).ok().flatten().is_some_and(|info| { + matches!( + info.color_type, + craft_image_codecs::RasterColorType::Bitmap | craft_image_codecs::RasterColorType::Gray | craft_image_codecs::RasterColorType::GrayAlpha + ) + }) } /// A JPEG, passed through. With `tag_rgb`, a JPEG keeps its own profile, and one without gets @@ -88,25 +86,28 @@ pub(super) fn jpeg(data: krilla::Data, icc: impl FnOnce() -> Option>, mo /// Whether krilla would write this PNG, GIF or WebP as untagged RGB: not grey, and no profile of /// its own. fn untagged_rgb(data: &[u8]) -> bool { - use image::ImageDecoder; - let Some(mut dec) = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format().ok().and_then(|r| r.into_decoder().ok()) else { - return false; - }; - let grey = matches!(dec.color_type(), image::ColorType::L8 | image::ColorType::La8 | image::ColorType::L16 | image::ColorType::La16); - !grey && dec.icc_profile().ok().flatten().is_none() + let Some(info) = craft_image_codecs::inspect_raster(data).ok().flatten() else { return false }; + !matches!( + info.color_type, + craft_image_codecs::RasterColorType::Bitmap | craft_image_codecs::RasterColorType::Gray | craft_image_codecs::RasterColorType::GrayAlpha + ) && embedded_icc(data).is_none() } -pub(super) fn lossless(data: &Arc>, fmt: Option, mode: RasterMode) -> Option { +pub(super) fn lossless(data: &Arc>, fmt: Option, mode: RasterMode) -> Option { // krilla reads PNG, GIF and WebP lazily: a damaged file is accepted here and fails the whole // export when the PDF is written. Decoding it first means it is skipped, like any other // image that can't be decoded. let rgba = designcraft_images::decode_rgba(data)?; let i = mode.interpolate; let direct = match fmt { - Some(image::ImageFormat::Png | image::ImageFormat::Gif | image::ImageFormat::WebP) if mode.tag_rgb && untagged_rgb(data) => None, - Some(image::ImageFormat::Png) => Image::from_png(data.clone().into(), i).ok(), - Some(image::ImageFormat::Gif) => Image::from_gif(data.clone().into(), i).ok(), - Some(image::ImageFormat::WebP) => Image::from_webp(data.clone().into(), i).ok(), + Some(craft_image_codecs::Format::Png | craft_image_codecs::Format::Gif | craft_image_codecs::Format::WebP) + if mode.tag_rgb && untagged_rgb(data) => + { + None + } + Some(craft_image_codecs::Format::Png) => Image::from_png(data.clone().into(), i).ok(), + Some(craft_image_codecs::Format::Gif) => Image::from_gif(data.clone().into(), i).ok(), + Some(craft_image_codecs::Format::WebP) => Image::from_webp(data.clone().into(), i).ok(), _ => None, }; direct.or_else(|| { @@ -177,21 +178,20 @@ pub(super) fn recompress(data: &[u8], mode: RasterMode) -> Option { { RECOMPRESS_DECODES.with(|n| n.set(n.get() + 1)); } - let img = image::load_from_memory(data).ok()?; - if img.color().has_alpha() && img.to_rgba8().pixels().any(|p| p[3] < 255) { + 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) { return None; } - let mut buf = Vec::new(); - let enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, 90); // A grey image stays a grey JPEG (#1426). - let grey = !img.color().has_color(); - if grey { - let l = img.to_luma8(); - image::ImageEncoder::write_image(enc, l.as_raw(), l.width(), l.height(), image::ExtendedColorType::L8).ok()?; + let grey = img.layout().is_gray(); + let jpeg_image = if grey { + img.convert(craft_image_codecs::ChannelLayout::Gray, craft_image_codecs::SampleType::U8) } else { - let rgb = img.to_rgb8(); - image::ImageEncoder::write_image(enc, rgb.as_raw(), rgb.width(), rgb.height(), image::ExtendedColorType::Rgb8).ok()?; - } + img.convert(craft_image_codecs::ChannelLayout::Rgb, craft_image_codecs::SampleType::U8) + }; + let options = craft_image_codecs::EncodeOptions { jpeg_quality: 90, ..Default::default() }; + let buf = craft_image_codecs::encode(&jpeg_image, craft_image_codecs::Format::Jpeg, &options).ok()?; jpeg(buf.into(), || embedded_icc(data).filter(|p| if grey { is_grey_icc(p) } else { is_rgb_icc(p) }), mode) } @@ -421,7 +421,7 @@ mod image_content_cache_tests { // Serialize the converted raster without an archival validator so the RGB-only pixel // contract stays independent of main's existing PDF/A interpolation validation failure. fn image_color_samples(image: Image) -> (String, Vec) { - use hayro_syntax::object::Name; + use craft_pdf::syntax::object::Name; let mut pdf = krilla::Document::new(); let size = Size::from_wh(20.0, 20.0).unwrap(); let mut page = pdf.start_page_with(PageSettings::new(size)); @@ -430,7 +430,7 @@ mod image_content_cache_tests { surface.finish(); page.finish(); let bytes = pdf.finish().unwrap(); - let pdf = hayro_syntax::Pdf::new(bytes).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap(); let stream = pdf .objects() .into_iter() @@ -466,7 +466,7 @@ mod image_content_cache_tests { #[cfg(test)] mod eps_tests { use super::*; - use hayro_syntax::object::Name; + use craft_pdf::syntax::object::Name; #[test] fn eps_tiff_preview_keeps_cmyk_inks_in_pdf() { @@ -522,7 +522,7 @@ mod eps_tests { for standard in [Standard::None, Standard::PdfX4] { let report = export_pdf_with_report(&doc, &Cache::new(), &PdfOptions { standard, ..Default::default() }).unwrap(); assert!(report.warnings.is_empty(), "{:?}", report.warnings); - let pdf = hayro_syntax::Pdf::new(report.bytes).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(report.bytes).unwrap(); let images: Vec<_> = pdf .objects() .into_iter() diff --git a/crates/pdf/src/optional_content.rs b/crates/pdf/src/optional_content.rs index 08dbbe0bd..a21d89103 100644 --- a/crates/pdf/src/optional_content.rs +++ b/crates/pdf/src/optional_content.rs @@ -1,6 +1,6 @@ -use hayro_syntax::Pdf; -use hayro_syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream}; -use hayro_syntax::reader::{Reader, ReaderExt}; +use craft_pdf::syntax::Pdf; +use craft_pdf::syntax::object::{Array, Dict, MaybeRef, Name, Object, ObjectIdentifier, Stream}; +use craft_pdf::syntax::reader::{Reader, ReaderExt}; use std::collections::HashSet; use std::sync::Arc; @@ -238,7 +238,7 @@ pub(crate) fn prepare(bytes: &Arc>, choice: Option<&[String]>) -> Result if group.get::>(b"Type").as_ref().map(AsRef::as_ref) != Some(b"OCG") { return Err(invalid()); } - let name = group.get::>(b"Name"); + let name = group.get::>(b"Name"); let is_hidden = match (choice, name.as_ref()) { (None, _) => !fallback && (off.contains(&id) || (base_off && !on.contains(&id))), (Some([]), None) => false, diff --git a/crates/pdf/src/pdfx.rs b/crates/pdf/src/pdfx.rs index 731a5989d..bae800bd5 100644 --- a/crates/pdf/src/pdfx.rs +++ b/crates/pdf/src/pdfx.rs @@ -5,10 +5,10 @@ use std::collections::{HashMap, HashSet}; -use hayro_syntax::content::UntypedIter; -use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; -use hayro_syntax::page::Resources; -use hayro_syntax::reader::ReaderExt; +use craft_pdf::syntax::content::UntypedIter; +use craft_pdf::syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; +use craft_pdf::syntax::page::Resources; +use craft_pdf::syntax::reader::ReaderExt; use craft_pdf::write::WriteError; use craft_pdf::write::backend as krilla; @@ -116,7 +116,7 @@ pub fn has_rgb_groups(pdf: &[u8]) -> bool { pub fn check_pdfx4(pdf: &[u8]) -> Vec { let mut issues = Vec::new(); let text = String::from_utf8_lossy(pdf); - let file = hayro_syntax::Pdf::new(pdf.to_vec()).ok(); + let file = craft_pdf::syntax::Pdf::new(pdf.to_vec()).ok(); if !pdf.starts_with(b"%PDF-1.") || pdf.get(7).is_none_or(|v| *v > b'6') { issues.push("the PDF version must be 1.6 or lower".into()); } @@ -161,12 +161,12 @@ pub fn check_pdfx4(pdf: &[u8]) -> Vec { /// The document information's `GTS_PDFXVersion` (`PDF/X-4`, `PDF/X-1:2003`, …), read as an /// object; `None` when the file can't be read or doesn't name one. pub fn pdfx_version(pdf: &[u8]) -> Option { - info_text(pdf, &hayro_syntax::Pdf::new(pdf.to_vec()).ok()?, b"GTS_PDFXVersion") + info_text(pdf, &craft_pdf::syntax::Pdf::new(pdf.to_vec()).ok()?, b"GTS_PDFXVersion") } /// The catalog's `GTS_PDFX` output intents, read as objects (whitespace and object streams /// don't matter). -pub(crate) fn pdfx_intents<'a>(file: &'a hayro_syntax::Pdf) -> Vec> { +pub(crate) fn pdfx_intents<'a>(file: &'a craft_pdf::syntax::Pdf) -> Vec> { let xref = file.xref(); let Some(catalog) = xref.get::>(xref.root_id()) else { return Vec::new() }; let Some(intents) = catalog.get::>(b"OutputIntents") else { return Vec::new() }; @@ -176,13 +176,13 @@ pub(crate) fn pdfx_intents<'a>(file: &'a hayro_syntax::Pdf) -> Vec> { /// The text entry `key` of the document information dictionary, trimmed: the `/Info` of the /// newest trailer that has one (classic or cross-reference stream, following `/Prev` a bounded /// number of times), else of the last `trailer` keyword in the file. -pub(crate) fn info_text(pdf: &[u8], file: &hayro_syntax::Pdf, key: &[u8]) -> Option { +pub(crate) fn info_text(pdf: &[u8], file: &craft_pdf::syntax::Pdf, key: &[u8]) -> Option { let lookup = |trailer: &Dict<'_>| -> Option { let info = match trailer.get_ref(b"Info") { Some(r) => file.xref().get::>(r.into())?, None => trailer.get::>(b"Info")?, }; - let value = info.get::>(key)?; + let value = info.get::>(key)?; Some(String::from_utf8_lossy(value.as_bytes()).trim().to_string()) }; let tail = pdf.get(rfind(pdf, b"startxref")?..).unwrap_or_default(); @@ -198,7 +198,7 @@ pub(crate) fn info_text(pdf: &[u8], file: &hayro_syntax::Pdf, key: &[u8]) -> Opt } // A damaged cross-reference offset: the dictionary after the last `trailer` keyword. let at = rfind(pdf, b"trailer")? + b"trailer".len(); - let mut reader = hayro_syntax::reader::Reader::new(pdf.get(at..)?); + let mut reader = craft_pdf::syntax::reader::Reader::new(pdf.get(at..)?); reader.skip_white_spaces_and_comments(); lookup(&reader.read_without_context::>()?) } @@ -209,7 +209,7 @@ pub(crate) fn info_text(pdf: &[u8], file: &hayro_syntax::Pdf, key: &[u8]) -> Opt /// This walks every page's content, the images, forms, patterns, shadings and soft masks it uses, /// and the transparency groups, and reports each kind of device RGB it finds with the pages it is /// on. Nothing is reported when the file has no CMYK PDF/X output intent. -fn device_rgb(file: Option<&hayro_syntax::Pdf>) -> Vec { +fn device_rgb(file: Option<&craft_pdf::syntax::Pdf>) -> Vec { let Some(file) = file else { return vec!["the file couldn't be read to check its colour spaces".into()]; }; @@ -249,7 +249,7 @@ fn device_rgb(file: Option<&hayro_syntax::Pdf>) -> Vec { } /// Whether the file's PDF/X output intent has a four-channel (CMYK) destination profile. -pub(crate) fn cmyk_output_intent(file: &hayro_syntax::Pdf) -> bool { +pub(crate) fn cmyk_output_intent(file: &craft_pdf::syntax::Pdf) -> bool { pdfx_intents(file).iter().any(|oi| oi.get::>(b"DestOutputProfile").and_then(|p| p.dict().get::(b"N")) == Some(4)) } diff --git a/crates/pdf/src/pdfx/fonts.rs b/crates/pdf/src/pdfx/fonts.rs index 2107f84eb..401d551b7 100644 --- a/crates/pdf/src/pdfx/fonts.rs +++ b/crates/pdf/src/pdfx/fonts.rs @@ -1,8 +1,8 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use hayro_syntax::content::UntypedIter; -use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; -use hayro_syntax::page::Resources; +use craft_pdf::syntax::content::UntypedIter; +use craft_pdf::syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; +use craft_pdf::syntax::page::Resources; use super::MAX_DEPTH; @@ -14,7 +14,7 @@ const LIMIT_WARNING: &str = "font scan operation limit reached: PDF/X-4 font emb const DEPTH_WARNING: &str = "font scan nesting depth limit reached: PDF/X-4 font embedding checks are incomplete"; pub(super) fn unembedded_fonts(pdf: &[u8]) -> Vec { - let Ok(file) = hayro_syntax::Pdf::new(pdf.to_vec()) else { return Vec::new() }; + let Ok(file) = craft_pdf::syntax::Pdf::new(pdf.to_vec()) else { return Vec::new() }; let mut found: BTreeMap> = BTreeMap::new(); let mut scan = FontScan::default(); for (i, page) in file.pages().iter().enumerate() { diff --git a/crates/pdf/src/pdfx/fonts/tests.rs b/crates/pdf/src/pdfx/fonts/tests.rs index c148cb076..f555f2651 100644 --- a/crates/pdf/src/pdfx/fonts/tests.rs +++ b/crates/pdf/src/pdfx/fonts/tests.rs @@ -47,7 +47,7 @@ fn pdf_objects(objects: &[String]) -> Vec { #[test] fn nested_form_fanout_is_memoized_and_reports_bottom_font() { let bytes = fanout_pdf(16); - let pdf = hayro_syntax::Pdf::new(bytes.clone()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes.clone()).unwrap(); let page = pdf.pages().first().unwrap(); let mut scan = FontScan::default(); let start = Instant::now(); diff --git a/crates/pdf/src/pdfx/tests.rs b/crates/pdf/src/pdfx/tests.rs index b35e897c2..6fc7fb90d 100644 --- a/crates/pdf/src/pdfx/tests.rs +++ b/crates/pdf/src/pdfx/tests.rs @@ -1,12 +1,12 @@ use craft_pdf::write::backend as krilla; use std::sync::Arc; +use craft_pdf::syntax::object::{Name, Object}; use designcraft_color::{Color, Swatch}; use designcraft_compose::Cache; use designcraft_doc::build::NewDocument; use designcraft_doc::{Asset, AssetId, Content, Document, Fill, Fitting, Graphic, Item, ItemId, Shape, SpreadRef}; use designcraft_geom::{Affine, Rect}; -use hayro_syntax::object::{Name, Object}; use image::{ExtendedColorType, ImageEncoder}; use crate::{ExportReport, PdfOptions, Standard, check_pdfx4, export_pdf_with_report}; @@ -117,7 +117,7 @@ pub(super) fn x4(d: &Document) -> ExportReport { /// The `/ColorSpace` of every image XObject: a name, or `[` for an array (ICCBased, Indexed…). fn image_spaces(bytes: &[u8]) -> Vec { - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).unwrap(); let mut out: Vec = pdf .objects() .into_iter() @@ -367,12 +367,12 @@ fn rgb_rasters() -> Vec<(&'static str, Vec)> { /// The decoded `/ICCBased` profile of every image XObject that has one. fn image_profiles(bytes: &[u8]) -> Vec> { - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).unwrap(); pdf.objects() .into_iter() .filter_map(|o| o.into_stream()) .filter(|s| s.dict().get::(b"Subtype").is_some_and(|n| n.as_str() == "Image")) - .filter_map(|s| match s.dict().get::(b"ColorSpace")?.iter::().nth(1)? { + .filter_map(|s| match s.dict().get::(b"ColorSpace")?.iter::().nth(1)? { Object::Stream(p) => Some(p.decoded().ok()?.into_owned()), _ => None, }) @@ -453,10 +453,10 @@ fn cmyk_tiff() -> Vec { #[test] fn xmp_states_trapped() { let r = x4(&Document::new(&NewDocument::default())); - let pdf = hayro_syntax::Pdf::new(r.bytes.clone()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(r.bytes.clone()).unwrap(); let xref = pdf.xref(); - let catalog = xref.get::(xref.root_id()).unwrap(); - let xmp = catalog.get::(b"Metadata").unwrap().decoded().unwrap().into_owned(); + let catalog = xref.get::(xref.root_id()).unwrap(); + let xmp = catalog.get::(b"Metadata").unwrap().decoded().unwrap().into_owned(); let xmp = String::from_utf8_lossy(&xmp); assert!(xmp.contains("xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\"") && xmp.contains("False"), "{xmp}"); assert!(String::from_utf8_lossy(&r.bytes).contains("/Trapped/False")); diff --git a/crates/pdf/src/pdfx1a.rs b/crates/pdf/src/pdfx1a.rs index aa8873083..67e0a583c 100644 --- a/crates/pdf/src/pdfx1a.rs +++ b/crates/pdf/src/pdfx1a.rs @@ -5,12 +5,12 @@ use std::collections::{HashMap, HashSet}; +use craft_pdf::syntax::content::UntypedIter; +use craft_pdf::syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; +use craft_pdf::syntax::page::Resources; use craft_pdf::write::backend::configure::PdfVersion; use craft_prepress::flatten::pdf::ReadOptions; use craft_prepress::flatten::{FlattenDocumentOptions, FlattenOptions, FlattenTotals, flatten_document}; -use hayro_syntax::content::UntypedIter; -use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier, Stream}; -use hayro_syntax::page::Resources; use crate::PdfError; use crate::pdfx::MAX_DEPTH; @@ -116,7 +116,7 @@ pub fn check_pdfx1a(pdf: &[u8]) -> Vec { fn checks(pdf: &[u8]) -> (Vec, Vec) { let mut issues = Vec::new(); let text = String::from_utf8_lossy(pdf); - let file = hayro_syntax::Pdf::new(pdf.to_vec()).ok(); + let file = craft_pdf::syntax::Pdf::new(pdf.to_vec()).ok(); if !pdf.starts_with(b"%PDF-1.") || pdf.get(7).is_none_or(|v| *v > b'4') { issues.push("the PDF version must be 1.4 or lower".into()); } @@ -151,7 +151,7 @@ fn checks(pdf: &[u8]) -> (Vec, Vec) { /// Transparency and color spaces, then the output intent and fonts, walked through every page's /// content and what it uses. -fn content(file: Option<&hayro_syntax::Pdf>) -> (Vec, Vec) { +fn content(file: Option<&craft_pdf::syntax::Pdf>) -> (Vec, Vec) { let Some(file) = file else { return (vec!["the file couldn't be read to check its transparency and color spaces".into()], Vec::new()); }; @@ -215,13 +215,13 @@ impl Intent { } /// The catalog's `GTS_PDFX` output intents. -fn pdfx_intents(file: &hayro_syntax::Pdf) -> Vec { +fn pdfx_intents(file: &craft_pdf::syntax::Pdf) -> Vec { let number = |n: i32| usize::try_from(n).ok(); crate::pdfx::pdfx_intents(file) .into_iter() .map(|oi| { let text = |key: &[u8]| { - oi.get::>(key) + oi.get::>(key) .map(|s| String::from_utf8_lossy(s.as_bytes()).trim().to_string()) .filter(|s| !s.is_empty()) }; @@ -243,7 +243,7 @@ fn pdfx_intents(file: &hayro_syntax::Pdf) -> Vec { /// What PDF/X-1a finds wrong with the file's output intent: a CMYK profile, ICC v2 (PDF 1.4 /// allows no later version), or none for a registered printing condition. -fn intent_issues(file: &hayro_syntax::Pdf) -> Vec { +fn intent_issues(file: &craft_pdf::syntax::Pdf) -> Vec { let Some(oi) = pdfx_intents(file).into_iter().next() else { return Vec::new() }; let mut issues = Vec::new(); if oi.has_profile { @@ -267,7 +267,7 @@ fn intent_issues(file: &hayro_syntax::Pdf) -> Vec { pub(crate) fn finish(pdf: &[u8]) -> Result, String> { let unreadable = || "the flattened file couldn't be read to add the PDF/X-1a identification".to_string(); let layout = Layout::read(pdf).ok_or_else(unreadable)?; - let file = hayro_syntax::Pdf::new(pdf.to_vec()).map_err(|_| unreadable())?; + let file = craft_pdf::syntax::Pdf::new(pdf.to_vec()).map_err(|_| unreadable())?; let mut bodies: HashMap> = HashMap::new(); let info = int_after(&layout.trailer, "/Info").ok_or_else(unreadable)?; let body = layout.body(pdf, info).ok_or_else(unreadable)?; diff --git a/crates/pdf/src/print_tests.rs b/crates/pdf/src/print_tests.rs index 6c5dafcb2..ccc646585 100644 --- a/crates/pdf/src/print_tests.rs +++ b/crates/pdf/src/print_tests.rs @@ -1,13 +1,13 @@ //! CMYK print output, read back from the exported PDF: blending spaces, the output intent, //! overprint and knockout groups. +use craft_pdf::syntax::Pdf; +use craft_pdf::syntax::object::{Array, Dict, Name, Object, Stream}; use designcraft_color::cms::{self, Cms, ColorSettings}; use designcraft_compose::Cache; use designcraft_doc::build::NewDocument; use designcraft_doc::{BlendSpace, Document, Fill, Item, ItemId, SpreadRef}; use designcraft_geom::Rect; -use hayro_syntax::Pdf; -use hayro_syntax::object::{Array, Dict, Name, Object, Stream}; use crate::{PdfOptions, export_pdf}; @@ -38,7 +38,7 @@ pub(crate) struct Intent { } fn text(d: &Dict<'_>, key: &[u8]) -> Option { - d.get::(key).map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned()) + d.get::(key).map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned()) } /// The catalog's `GTS_PDFX` output intent. diff --git a/crates/pdf/src/registration_tests.rs b/crates/pdf/src/registration_tests.rs index 4d67aac01..151ce772f 100644 --- a/crates/pdf/src/registration_tests.rs +++ b/crates/pdf/src/registration_tests.rs @@ -76,8 +76,8 @@ fn registrations(action: impl FnOnce()) -> usize { REGISTRATIONS.get() } -fn check_vector_artwork(pdf: &hayro_syntax::Pdf) { - use hayro_syntax::object::{Name, Object}; +fn check_vector_artwork(pdf: &craft_pdf::syntax::Pdf) { + use craft_pdf::syntax::object::{Name, Object}; let mut found = false; for object in pdf.objects() { @@ -94,9 +94,9 @@ fn check_vector_artwork(pdf: &hayro_syntax::Pdf) { } fn check_unlayered_pdf(bytes: &[u8], pages: usize, artwork: bool) { - use hayro_syntax::object::Dict; + use craft_pdf::syntax::object::Dict; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).unwrap(); assert_eq!(pdf.pages().len(), pages); let catalog = pdf.xref().get::>(pdf.xref().root_id()).unwrap(); assert!(!catalog.contains_key(b"OCProperties"), "unlayered output must not acquire optional content"); @@ -194,9 +194,9 @@ fn layered_and_resource_only_inputs_register_optional_content() { } fn check_registered(bytes: &[u8], hidden: bool) { - use hayro_syntax::object::{Array, Dict, Name, Object, ObjectIdentifier}; + use craft_pdf::syntax::object::{Array, Dict, Name, Object, ObjectIdentifier}; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).unwrap(); check_vector_artwork(&pdf); let xref = pdf.xref(); let catalog = xref.get::>(xref.root_id()).unwrap(); @@ -206,7 +206,7 @@ fn check_registered(bytes: &[u8], hidden: bool) { let group_id: ObjectIdentifier = groups.raw_iter().next().unwrap().as_obj_ref().unwrap().into(); let group = xref.get::>(group_id).unwrap(); assert_eq!(group.get::>(b"Type").unwrap().as_ref(), b"OCG"); - assert_eq!(group.get::>(b"Name").unwrap().as_bytes(), b"Hidden"); + assert_eq!(group.get::>(b"Name").unwrap().as_bytes(), b"Hidden"); let config = properties.get::>(b"D").unwrap(); if hidden { diff --git a/crates/pdf/src/rtl_tests.rs b/crates/pdf/src/rtl_tests.rs index 82974029e..593e1ca79 100644 --- a/crates/pdf/src/rtl_tests.rs +++ b/crates/pdf/src/rtl_tests.rs @@ -47,9 +47,9 @@ fn rtl() -> ParaAttrs { /// The ActualText of every marked-content span on the first page. fn actual_texts(bytes: &[u8]) -> Vec { - use hayro_syntax::content::ops::TypedInstruction; - use hayro_syntax::object::{Dict, Object}; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + use craft_pdf::syntax::content::ops::TypedInstruction; + use craft_pdf::syntax::object::{Dict, Object}; + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); let page = &pdf.pages()[0]; let mut out = Vec::new(); let mut ops = page.typed_operations(); @@ -57,7 +57,7 @@ fn actual_texts(bytes: &[u8]) -> Vec { let TypedInstruction::BeginMarkedContentWithProperties(op) = op else { continue }; let Object::Dict(props) = op.1 else { continue }; let props: &Dict<'_> = props; - let Some(s) = props.get::(b"ActualText") else { continue }; + let Some(s) = props.get::(b"ActualText") else { continue }; let units: Vec = s.as_bytes().as_chunks::<2>().0.iter().map(|&b| u16::from_be_bytes(b)).collect(); let text = String::from_utf16(&units).expect("UTF-16"); out.push(text.trim_start_matches('\u{FEFF}').to_string()); @@ -112,7 +112,7 @@ fn glyph_origins(bytes: &[u8]) -> Vec { fn pop_clip_path(&mut self) {} fn pop_transparency_group(&mut self) {} } - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); let cache = InterpreterCache::new(); let mut ctx = Context::new(kurbo::Affine::IDENTITY, kurbo::Rect::new(0.0, 0.0, 1.0, 1.0), &cache, pdf.xref(), InterpreterSettings::default()); let mut rec = Rec(Vec::new()); @@ -183,9 +183,9 @@ fn arabic_ligatures_extract_their_letters() { /// How many marked-content sections the first page opens while an ActualText span is open. /// Viewers such as poppler drop the text before an ActualText span nested in another. fn opened_inside_actual_text(bytes: &[u8]) -> usize { - use hayro_syntax::content::ops::TypedInstruction; - use hayro_syntax::object::Object; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + use craft_pdf::syntax::content::ops::TypedInstruction; + use craft_pdf::syntax::object::Object; + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); let page = &pdf.pages()[0]; let (mut open, mut nested) = (Vec::new(), 0); let mut ops = page.typed_operations(); diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index ac32be45b..452b5d1b5 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -29,12 +29,12 @@ fn uncompressed(d: &Document, opts: PdfOptions) -> String { /// Text of every page through hayro's interpreter (glyph → Unicode via ToUnicode / ActualText). pub(crate) fn extract_text(bytes: &[u8]) -> Vec { + use craft_pdf::syntax::Pdf; use hayro_interpret::font::Glyph; use hayro_interpret::{ BlendMode, ClipPath, Context, Device, GlyphDrawMode, Image, InterpreterCache, InterpreterSettings, Paint, PathDrawMode, SoftMask, interpret_page, }; - use hayro_syntax::Pdf; struct Ex(String); impl Device<'_> for Ex { fn set_soft_mask(&mut self, _: Option>) {} @@ -72,7 +72,7 @@ fn exports_valid_pdf_with_one_page_per_page() { let d = doc_with_text("Hello DesignCraft"); let bytes = export_pdf(&d, &Cache::new(), &PdfOptions::default()).unwrap(); assert!(bytes.starts_with(b"%PDF-1.7")); - let pdf = hayro_syntax::Pdf::new(bytes.clone()).expect("parse"); + let pdf = craft_pdf::syntax::Pdf::new(bytes.clone()).expect("parse"); assert_eq!(pdf.pages().len(), 2); } @@ -173,7 +173,7 @@ fn spreads_and_ranges() { let n = d.page_count(); assert_eq!(n, 4); let bytes = export_pdf(&d, &Cache::new(), &PdfOptions { spreads: true, ..Default::default() }).unwrap(); - let pages = hayro_syntax::Pdf::new(bytes).unwrap().pages().len(); + let pages = craft_pdf::syntax::Pdf::new(bytes).unwrap().pages().len(); assert_eq!(pages, d.spreads.len()); assert_eq!(parse_page_range("1-2, 4", 4).unwrap(), vec![0, 1, 3]); assert_eq!(parse_page_range("3-", 4).unwrap(), vec![2, 3]); @@ -187,7 +187,7 @@ fn spreads_and_ranges() { assert!(parse_page_range("5", 4).is_err()); assert!(parse_page_range("x", 4).is_err()); let bytes = export_pdf(&d, &Cache::new(), &PdfOptions { pages: Some(vec![1, 2]), ..Default::default() }).unwrap(); - assert_eq!(hayro_syntax::Pdf::new(bytes).unwrap().pages().len(), 2); + assert_eq!(craft_pdf::syntax::Pdf::new(bytes).unwrap().pages().len(), 2); assert_eq!(export_pdf(&d, &Cache::new(), &PdfOptions { pages: Some(vec![9]), ..Default::default() }), Err(PdfError::BadPage(10))); } @@ -221,7 +221,10 @@ fn standards() { assert_eq!((intent.identifier.as_str(), intent.condition.as_str()), ("FOGRA39", "Coated FOGRA39 (fitted, craft-cms)")); // A plain export fails the checks; both read with the same pages. let plain = export_pdf(&d, &Cache::new(), &PdfOptions::default()).unwrap(); - assert_eq!(hayro_syntax::Pdf::new(r.bytes.clone()).unwrap().pages().len(), hayro_syntax::Pdf::new(plain.clone()).unwrap().pages().len()); + assert_eq!( + craft_pdf::syntax::Pdf::new(r.bytes.clone()).unwrap().pages().len(), + craft_pdf::syntax::Pdf::new(plain.clone()).unwrap().pages().len() + ); assert!(crate::check_pdfx4(&plain).len() >= 3); assert_eq!(Standard::parse("PDF/X-4"), Some(Standard::PdfX4)); assert_eq!(Standard::parse("pdfa-2b"), Some(Standard::PdfA2b)); @@ -397,8 +400,8 @@ fn an_image_past_the_decode_budget_is_skipped_with_a_warning() { /// Every image XObject in a PDF: its /ColorSpace name (`?` when not a name) and decoded samples. fn image_xobjects(bytes: &[u8]) -> Vec<(String, Vec)> { - use hayro_syntax::object::Name; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + use craft_pdf::syntax::object::Name; + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); pdf.objects() .into_iter() .filter_map(|o| o.into_stream()) @@ -462,8 +465,8 @@ fn raster(fmt: image::ImageFormat, alpha: bool) -> Vec { /// Each image XObject's /Interpolate value (`None` when absent). fn interpolate_flags(bytes: &[u8]) -> Vec> { - use hayro_syntax::object::Name; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + use craft_pdf::syntax::object::Name; + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); pdf.objects() .into_iter() .filter_map(|o| o.into_stream()) @@ -529,7 +532,7 @@ fn drawn(bytes: &[u8]) -> (Vec<(String, kurbo::Rect)>, Vec) { fn pop_clip_path(&mut self) {} fn pop_transparency_group(&mut self) {} } - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); let cache = InterpreterCache::new(); let page = &pdf.pages()[0]; let mut ctx = Context::new(kurbo::Affine::IDENTITY, kurbo::Rect::new(0.0, 0.0, 1.0, 1.0), &cache, pdf.xref(), InterpreterSettings::default()); @@ -584,7 +587,7 @@ fn paint_events(bytes: &[u8], groups: bool) -> Vec<(String, f32)> { } } } - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).expect("parse"); + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).expect("parse"); let cache = InterpreterCache::new(); let page = &pdf.pages()[0]; let mut ctx = Context::new(kurbo::Affine::IDENTITY, kurbo::Rect::new(0.0, 0.0, 1.0, 1.0), &cache, pdf.xref(), InterpreterSettings::default()); diff --git a/crates/pdf/src/x1a_tests.rs b/crates/pdf/src/x1a_tests.rs index c79aaa172..7a7cb3277 100644 --- a/crates/pdf/src/x1a_tests.rs +++ b/crates/pdf/src/x1a_tests.rs @@ -5,13 +5,13 @@ use std::collections::HashMap; use std::sync::Arc; +use craft_pdf::syntax::Pdf; +use craft_pdf::syntax::object::{Dict, Name, Object}; use designcraft_color::{Color, ColorType, Swatch, SwatchValue}; use designcraft_compose::Cache; use designcraft_doc::build::NewDocument; use designcraft_doc::{Content, Document, Fill, Item, ItemId, ParaFormat, SpreadRef}; use designcraft_geom::Rect; -use hayro_syntax::Pdf; -use hayro_syntax::object::{Dict, Name, Object}; use crate::tests::extract_text; use crate::{PdfOptions, ShadowMask, Standard, check_pdfx1a, export_pdf_with_report}; @@ -114,8 +114,8 @@ fn x1a_export_flattens_transparency_into_inks() { // The default Coated FOGRA39 is a registered condition: no (ICC v4) profile in the file. let pdf = Pdf::new(bytes.to_vec()).unwrap(); let oi = pdfx_intent(&pdf); - assert_eq!(oi.get::>(b"OutputConditionIdentifier").unwrap().as_bytes(), b"FOGRA39"); - assert_eq!(oi.get::>(b"RegistryName").unwrap().as_bytes(), b"http://www.color.org"); + assert_eq!(oi.get::>(b"OutputConditionIdentifier").unwrap().as_bytes(), b"FOGRA39"); + assert_eq!(oi.get::>(b"RegistryName").unwrap().as_bytes(), b"http://www.color.org"); assert!(!oi.contains_key(b"DestOutputProfile"), "no destination profile"); let profiles = pdf.objects().into_iter().filter(|o| matches!(o, Object::Stream(s) if s.dict().get::(b"N") == Some(4))).count(); assert_eq!(profiles, 0, "the profile is left out of the file"); @@ -144,7 +144,7 @@ fn x1a_export_flattens_transparency_into_inks() { fn pdfx_intent<'a>(pdf: &'a Pdf) -> Dict<'a> { let xref = pdf.xref(); let catalog = xref.get::>(xref.root_id()).unwrap(); - let intents = catalog.get::>(b"OutputIntents").unwrap(); + let intents = catalog.get::>(b"OutputIntents").unwrap(); intents.iter::>().find(|oi| oi.get::>(b"S").is_some_and(|s| s.as_ref() == b"GTS_PDFX")).unwrap() } diff --git a/crates/pdf/tests/it/gradient_color_spaces.rs b/crates/pdf/tests/it/gradient_color_spaces.rs index 15a2c5233..fedf8fd9b 100644 --- a/crates/pdf/tests/it/gradient_color_spaces.rs +++ b/crates/pdf/tests/it/gradient_color_spaces.rs @@ -1,13 +1,13 @@ //! Synthetic regressions for gradients that previously aborted inside the PDF writer. +use craft_pdf::syntax::{ + Pdf, + object::{Array, Dict, Name}, +}; use designcraft_color::{Color, Gradient, GradientKind, GradientStop, Swatch, SwatchValue}; use designcraft_compose::Cache; use designcraft_doc::{Document, Fill, Item, ItemId, Shape, SpreadRef, build::NewDocument}; use designcraft_geom::{Rect, shapes}; use designcraft_pdf::{PdfOptions, Standard, export_pdf_with_report}; -use hayro_syntax::{ - Pdf, - object::{Array, Dict, Name}, -}; fn document(colors: &[Color], kind: GradientKind, midpoint: f32, opacity: f32) -> Document { let mut doc = Document::new(&NewDocument { width: 120.0, height: 120.0, facing_pages: false, ..Default::default() }); diff --git a/crates/pdf/tests/it/spot_gradients.rs b/crates/pdf/tests/it/spot_gradients.rs index eac962bc9..a47454c22 100644 --- a/crates/pdf/tests/it/spot_gradients.rs +++ b/crates/pdf/tests/it/spot_gradients.rs @@ -1,14 +1,14 @@ //! Gradients on spot swatches shade in their inks: Separation for one spot, DeviceN for a spot //! with process inks or a second spot. +use craft_pdf::syntax::{ + Pdf, + object::{Array, Dict, Name}, +}; use designcraft_color::{Color, ColorType, Gradient, GradientKind, GradientStop, Swatch, SwatchValue}; use designcraft_compose::Cache; use designcraft_doc::{Document, Fill, Item, ItemId, Shape, SpreadRef, build::NewDocument}; use designcraft_geom::{Rect, shapes}; use designcraft_pdf::{PdfOptions, Standard, export_pdf_with_report}; -use hayro_syntax::{ - Pdf, - object::{Array, Dict, Name}, -}; const CYAN: &str = "C=100 M=0 Y=0 K=0"; diff --git a/crates/pdf/tests/support/image_content_cache_contract.rs b/crates/pdf/tests/support/image_content_cache_contract.rs index a35fc464e..fc2b53c9d 100644 --- a/crates/pdf/tests/support/image_content_cache_contract.rs +++ b/crates/pdf/tests/support/image_content_cache_contract.rs @@ -93,7 +93,7 @@ fn drawn_content(bytes: Vec) -> (Vec, Vec<([u8; 4], kurbo::Rect) fn pop_clip_path(&mut self) {} fn pop_transparency_group(&mut self) {} } - let pdf = hayro_syntax::Pdf::new(bytes).unwrap(); + let pdf = craft_pdf::syntax::Pdf::new(bytes).unwrap(); let cache = InterpreterCache::new(); let mut sink = Sink(Vec::new(), Vec::new()); for page in pdf.pages().iter() { @@ -182,8 +182,8 @@ struct ImageObject { } fn image_objects(bytes: &[u8]) -> Vec { - use hayro_syntax::object::{Name, Stream}; - let pdf = hayro_syntax::Pdf::new(bytes.to_vec()).unwrap(); + use craft_pdf::syntax::object::{Name, Stream}; + let pdf = craft_pdf::syntax::Pdf::new(bytes.to_vec()).unwrap(); pdf.objects() .into_iter() .filter_map(|o| o.into_stream()) diff --git a/crates/textimport/src/docx.rs b/crates/textimport/src/docx.rs index ba869a599..4cc43f260 100644 --- a/crates/textimport/src/docx.rs +++ b/crates/textimport/src/docx.rs @@ -26,40 +26,43 @@ fn style_names(styles: &StyleSheet) -> HashMap { } fn char_attrs(p: &WordCharProps) -> CharAttrs { - let mut out = CharAttrs::default(); - out.font_family = p.font.as_ref().map(|s| capped_name(s)); - out.font_style = match (p.bold, p.italic) { - (Some(true), Some(true)) => Some("Bold Italic".into()), - (Some(true), _) => Some("Bold".into()), - (_, Some(true)) => Some("Italic".into()), - (Some(false), Some(false)) => Some("Regular".into()), - _ => None, - }; - out.size = p.size.map(f64::from).and_then(designcraft_doc::attrs::imported_type_size); - out.underline = p.underline.map(|u| u != Underline::None); - out.strikethrough = p.strike.or(p.double_strike); - out.capitalization = p.caps.filter(|&on| on).map(|_| designcraft_doc::Capitalization::AllCaps); - out.position = p.vert_align.map(|v| match v { - VertAlign::Superscript => designcraft_doc::Position::Superscript, - VertAlign::Subscript => designcraft_doc::Position::Subscript, - VertAlign::Baseline => designcraft_doc::Position::Normal, - }); - out + CharAttrs { + font_family: p.font.as_ref().map(|s| capped_name(s)), + font_style: match (p.bold, p.italic) { + (Some(true), Some(true)) => Some("Bold Italic".into()), + (Some(true), _) => Some("Bold".into()), + (_, Some(true)) => Some("Italic".into()), + (Some(false), Some(false)) => Some("Regular".into()), + _ => None, + }, + size: p.size.map(f64::from).and_then(designcraft_doc::attrs::imported_type_size), + underline: p.underline.map(|u| u != Underline::None), + strikethrough: p.strike.or(p.double_strike), + capitalization: p.caps.filter(|&on| on).map(|_| designcraft_doc::Capitalization::AllCaps), + position: p.vert_align.map(|v| match v { + VertAlign::Superscript => designcraft_doc::Position::Superscript, + VertAlign::Subscript => designcraft_doc::Position::Subscript, + VertAlign::Baseline => designcraft_doc::Position::Normal, + }), + ..Default::default() + } } fn para_attrs(p: &WordParaProps, doc: &craft_wordmodel::Document) -> ParaAttrs { - let mut out = ParaAttrs::default(); - out.align = p.align.map(|a| match a { - WordAlign::Center => designcraft_doc::Align::Center, - WordAlign::Right => designcraft_doc::Align::Right, - WordAlign::Justify | WordAlign::Distribute => designcraft_doc::Align::LeftJustified, - WordAlign::Left => designcraft_doc::Align::Left, - }); - out.left_indent = p.indent_left.map(f64::from).and_then(designcraft_doc::attrs::imported_measure); - out.right_indent = p.indent_right.map(f64::from).and_then(designcraft_doc::attrs::imported_measure); - out.first_line_indent = p.indent_first.map(f64::from).and_then(designcraft_doc::attrs::imported_measure); - out.space_before = p.space_before.map(f64::from).and_then(designcraft_doc::attrs::imported_measure); - out.space_after = p.space_after.map(f64::from).and_then(designcraft_doc::attrs::imported_measure); + let mut out = ParaAttrs { + align: p.align.map(|a| match a { + WordAlign::Center => designcraft_doc::Align::Center, + WordAlign::Right => designcraft_doc::Align::Right, + WordAlign::Justify | WordAlign::Distribute => designcraft_doc::Align::LeftJustified, + WordAlign::Left => designcraft_doc::Align::Left, + }), + left_indent: p.indent_left.map(f64::from).and_then(designcraft_doc::attrs::imported_measure), + right_indent: p.indent_right.map(f64::from).and_then(designcraft_doc::attrs::imported_measure), + first_line_indent: p.indent_first.map(f64::from).and_then(designcraft_doc::attrs::imported_measure), + space_before: p.space_before.map(f64::from).and_then(designcraft_doc::attrs::imported_measure), + space_after: p.space_after.map(f64::from).and_then(designcraft_doc::attrs::imported_measure), + ..Default::default() + }; if let Some(numbering) = p.numbering.filter(|n| n.num != 0) { let definition = doc.numbering.nums.iter().find(|n| n.id == numbering.num).and_then(|n| { n.level_overrides.iter().find(|(level, _)| *level == numbering.level).map(|(_, def)| def).or_else(|| { @@ -165,19 +168,19 @@ fn word_text(c: char) -> char { } } -fn append_paragraph( - doc: &craft_wordmodel::Document, - p: &Paragraph, - st: &mut Story, - names: &HashMap, - warned_objects: &mut bool, - notes_done: &mut HashSet, - note_bytes: &mut usize, - warnings: &mut Vec, -) { +struct ImportContext<'a> { + doc: &'a craft_wordmodel::Document, + names: &'a HashMap, + warned_objects: bool, + notes_done: HashSet, + note_bytes: usize, + warnings: Vec, +} + +fn append_paragraph(ctx: &mut ImportContext<'_>, p: &Paragraph, st: &mut Story) { let para_at = st.len(); - let style = p.props.style.as_ref().and_then(|id| names.get(id)).cloned().unwrap_or_else(|| BASIC_PARAGRAPH.into()); - st.format_paras(para_at..para_at, |fmt| *fmt = ParaFormat { style: style.clone(), para: para_attrs(&p.props, doc), ..Default::default() }); + let style = p.props.style.as_ref().and_then(|id| ctx.names.get(id)).cloned().unwrap_or_else(|| BASIC_PARAGRAPH.into()); + st.format_paras(para_at..para_at, |fmt| *fmt = ParaFormat { style: style.clone(), para: para_attrs(&p.props, ctx.doc), ..Default::default() }); let mut object_i = 0usize; let mut last_format: Option = None; @@ -199,7 +202,7 @@ fn append_paragraph( }) .find(|(range, _)| range.contains(&at)) .map(|(_, r)| r); - let fmt = run.map(|r| char_format(&r.props, names)).unwrap_or_default(); + let fmt = run.map(|r| char_format(&r.props, ctx.names)).unwrap_or_default(); if last_format.as_ref().is_some_and(|prev| prev != &fmt) { flush(st, &mut last_format, &mut chunk); } @@ -209,40 +212,40 @@ fn append_paragraph( if let Some(object) = p.objects.get(object_i) { match object { InlineObject::NoteRef { kind: NoteKind::Footnote, id, .. } => { - let mut text = doc.parts.get(id).map(|part| plain_blocks(&part.blocks)).unwrap_or_default(); - if notes_done.contains(id) { + let mut text = ctx.doc.parts.get(id).map(|part| plain_blocks(&part.blocks)).unwrap_or_default(); + if ctx.notes_done.contains(id) { text.clear(); add_warning( - warnings, + &mut ctx.warnings, format!("Footnote {id} is referenced more than once in the Word file: the text is kept at its first reference."), ); - } else if note_bytes.saturating_add(text.len()) > MAX_NOTE_BYTES { + } else if ctx.note_bytes.saturating_add(text.len()) > MAX_NOTE_BYTES { text.clear(); add_warning( - warnings, + &mut ctx.warnings, format!("The Word file's footnotes are longer than {} MB: the rest were imported empty.", MAX_NOTE_BYTES / (1 << 20)), ); } else { - notes_done.insert(*id); - *note_bytes = note_bytes.saturating_add(text.len()); + ctx.notes_done.insert(*id); + ctx.note_bytes = ctx.note_bytes.saturating_add(text.len()); } let _ = st.insert_note(st.len(), &text, ParaFormat::default()); } InlineObject::NoteRef { .. } => { - *warned_objects = true; + ctx.warned_objects = true; let _ = st.insert_note(st.len(), "", ParaFormat::default()); } other => { let text = other.plain_text(); if text.is_empty() { - *warned_objects = true; + ctx.warned_objects = true; } else { st.insert_with(st.len(), text, last_format.clone().unwrap_or_default()); } } } } else { - *warned_objects = true; + ctx.warned_objects = true; } object_i = object_i.saturating_add(1); } else { @@ -252,16 +255,7 @@ fn append_paragraph( flush(st, &mut last_format, &mut chunk); } -fn append_blocks( - doc: &craft_wordmodel::Document, - blocks: &[std::sync::Arc], - st: &mut Story, - names: &HashMap, - warned_objects: &mut bool, - notes_done: &mut HashSet, - note_bytes: &mut usize, - warnings: &mut Vec, -) { +fn append_blocks(ctx: &mut ImportContext<'_>, blocks: &[std::sync::Arc], st: &mut Story) { let mut first = st.is_empty(); for block in blocks { match block.as_ref() { @@ -270,7 +264,7 @@ fn append_blocks( st.insert(st.len(), "\n"); } first = false; - append_paragraph(doc, p, st, names, warned_objects, notes_done, note_bytes, warnings); + append_paragraph(ctx, p, st); } Block::Table(table) => { let rows: Vec> = table.rows.iter().map(|r| r.cells.iter().map(|c| plain_blocks(&c.blocks)).collect()).collect(); @@ -289,14 +283,11 @@ pub fn import(bytes: &[u8]) -> Result { let doc = craft_docx::read(bytes).map_err(|e| ImportError::Corrupt(e.to_string()))?; let (mut para_styles, mut char_styles, names) = style_records(&doc.styles, &doc); let mut story = Story::new(StoryId(0)); - let mut warned_objects = false; - let mut notes_done = HashSet::new(); - let mut note_bytes = 0usize; - let mut warnings = Vec::new(); - append_blocks(&doc, &doc.body, &mut story, &names, &mut warned_objects, &mut notes_done, &mut note_bytes, &mut warnings); + let mut ctx = ImportContext { doc: &doc, names: &names, warned_objects: false, notes_done: HashSet::new(), note_bytes: 0, warnings: Vec::new() }; + append_blocks(&mut ctx, &doc.body, &mut story); story.end_paragraphs_at_breaks(); - if warned_objects { - add_warning(&mut warnings, "Images, drawings and endnotes in the Word file were not imported (place them separately).".into()); + if ctx.warned_objects { + add_warning(&mut ctx.warnings, "Images, drawings and endnotes in the Word file were not imported (place them separately).".into()); } let used_p: HashSet = story.paras.iter().map(|p| p.style.clone()).collect(); let used_c: HashSet = story.runs().map(|(_, f)| f.style.clone()).collect(); @@ -314,7 +305,7 @@ pub fn import(bytes: &[u8]) -> Result { }; keep(&mut para_styles, &used_p); keep(&mut char_styles, &used_c); - Ok(Imported { story, para_styles, char_styles, warnings }) + Ok(Imported { story, para_styles, char_styles, warnings: ctx.warnings }) } fn add_warning(warnings: &mut Vec, message: String) {