Route PDF parsing and raster import through craft-libs

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 01:19:41 -05:00
1 parent fcb571c52a
commit 32348da073
21 files changed
+207 -210

No files matched your search

Generated
+4 -4
View File
@@ -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",
+2 -2
View File
@@ -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.
+2 -3
View File
@@ -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"] }
+11 -7
View File
@@ -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<hayro_syntax::object::ObjectIdentifier>), Option<(krilla::pdf::PdfDocument, usize)>>,
pdfs: HashMap<(AssetId, Vec<craft_pdf::syntax::object::ObjectIdentifier>), Option<(krilla::pdf::PdfDocument, usize)>>,
variants: HashMap<(AssetId, Option<Vec<String>>), 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");
}
+4 -4
View File
@@ -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<Vec<u8>>) -> 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);
+35 -35
View File
@@ -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<Vec<u8>> {
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<Vec<u8>>, 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<Vec<u8>>, fmt: Option<image::ImageFormat>, mode: RasterMode) -> Option<Image> {
pub(super) fn lossless(data: &Arc<Vec<u8>>, fmt: Option<craft_image_codecs::Format>, mode: RasterMode) -> Option<Image> {
// 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<Image> {
{
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<u8>) {
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()
+4 -4
View File
@@ -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<Vec<u8>>, choice: Option<&[String]>) -> Result
if group.get::<Name<'_>>(b"Type").as_ref().map(AsRef::as_ref) != Some(b"OCG") {
return Err(invalid());
}
let name = group.get::<hayro_syntax::object::String<'_>>(b"Name");
let name = group.get::<craft_pdf::syntax::object::String<'_>>(b"Name");
let is_hidden = match (choice, name.as_ref()) {
(None, _) => !fallback && (off.contains(&id) || (base_off && !on.contains(&id))),
(Some([]), None) => false,
+12 -12
View File
@@ -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<String> {
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<String> {
/// 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<String> {
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<Dict<'a>> {
pub(crate) fn pdfx_intents<'a>(file: &'a craft_pdf::syntax::Pdf) -> Vec<Dict<'a>> {
let xref = file.xref();
let Some(catalog) = xref.get::<Dict<'_>>(xref.root_id()) else { return Vec::new() };
let Some(intents) = catalog.get::<Array<'_>>(b"OutputIntents") else { return Vec::new() };
@@ -176,13 +176,13 @@ pub(crate) fn pdfx_intents<'a>(file: &'a hayro_syntax::Pdf) -> Vec<Dict<'a>> {
/// 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<String> {
pub(crate) fn info_text(pdf: &[u8], file: &craft_pdf::syntax::Pdf, key: &[u8]) -> Option<String> {
let lookup = |trailer: &Dict<'_>| -> Option<String> {
let info = match trailer.get_ref(b"Info") {
Some(r) => file.xref().get::<Dict<'_>>(r.into())?,
None => trailer.get::<Dict<'_>>(b"Info")?,
};
let value = info.get::<hayro_syntax::object::String<'_>>(key)?;
let value = info.get::<craft_pdf::syntax::object::String<'_>>(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::<Dict<'_>>()?)
}
@@ -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<String> {
fn device_rgb(file: Option<&craft_pdf::syntax::Pdf>) -> Vec<String> {
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<String> {
}
/// 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::<Stream<'_>>(b"DestOutputProfile").and_then(|p| p.dict().get::<i32>(b"N")) == Some(4))
}
+4 -4
View File
@@ -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<String> {
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<String, Vec<usize>> = BTreeMap::new();
let mut scan = FontScan::default();
for (i, page) in file.pages().iter().enumerate() {
+1 -1
View File
@@ -47,7 +47,7 @@ fn pdf_objects(objects: &[String]) -> Vec<u8> {
#[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();
+7 -7
View File
@@ -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<String> {
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<String> = pdf
.objects()
.into_iter()
@@ -367,12 +367,12 @@ fn rgb_rasters() -> Vec<(&'static str, Vec<u8>)> {
/// The decoded `/ICCBased` profile of every image XObject that has one.
fn image_profiles(bytes: &[u8]) -> Vec<Vec<u8>> {
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::<Name>(b"Subtype").is_some_and(|n| n.as_str() == "Image"))
.filter_map(|s| match s.dict().get::<hayro_syntax::object::Array>(b"ColorSpace")?.iter::<Object>().nth(1)? {
.filter_map(|s| match s.dict().get::<craft_pdf::syntax::object::Array>(b"ColorSpace")?.iter::<Object>().nth(1)? {
Object::Stream(p) => Some(p.decoded().ok()?.into_owned()),
_ => None,
})
@@ -453,10 +453,10 @@ fn cmyk_tiff() -> Vec<u8> {
#[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::<hayro_syntax::object::Dict>(xref.root_id()).unwrap();
let xmp = catalog.get::<hayro_syntax::object::Stream>(b"Metadata").unwrap().decoded().unwrap().into_owned();
let catalog = xref.get::<craft_pdf::syntax::object::Dict>(xref.root_id()).unwrap();
let xmp = catalog.get::<craft_pdf::syntax::object::Stream>(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("<pdf:Trapped>False</pdf:Trapped>"), "{xmp}");
assert!(String::from_utf8_lossy(&r.bytes).contains("/Trapped/False"));
+9 -9
View File
@@ -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<String> {
fn checks(pdf: &[u8]) -> (Vec<String>, Vec<String>) {
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<String>, Vec<String>) {
/// 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<String>, Vec<String>) {
fn content(file: Option<&craft_pdf::syntax::Pdf>) -> (Vec<String>, Vec<String>) {
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<Intent> {
fn pdfx_intents(file: &craft_pdf::syntax::Pdf) -> Vec<Intent> {
let number = |n: i32| usize::try_from(n).ok();
crate::pdfx::pdfx_intents(file)
.into_iter()
.map(|oi| {
let text = |key: &[u8]| {
oi.get::<hayro_syntax::object::String<'_>>(key)
oi.get::<craft_pdf::syntax::object::String<'_>>(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<Intent> {
/// 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<String> {
fn intent_issues(file: &craft_pdf::syntax::Pdf) -> Vec<String> {
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<String> {
pub(crate) fn finish(pdf: &[u8]) -> Result<Vec<u8>, 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<usize, Vec<u8>> = HashMap::new();
let info = int_after(&layout.trailer, "/Info").ok_or_else(unreadable)?;
let body = layout.body(pdf, info).ok_or_else(unreadable)?;
+3 -3
View File
@@ -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<String> {
d.get::<hayro_syntax::object::String>(key).map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned())
d.get::<craft_pdf::syntax::object::String>(key).map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned())
}
/// The catalog's `GTS_PDFX` output intent.
+7 -7
View File
@@ -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::<Dict<'_>>(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::<Dict<'_>>(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::<Dict<'_>>(group_id).unwrap();
assert_eq!(group.get::<Name<'_>>(b"Type").unwrap().as_ref(), b"OCG");
assert_eq!(group.get::<hayro_syntax::object::String<'_>>(b"Name").unwrap().as_bytes(), b"Hidden");
assert_eq!(group.get::<craft_pdf::syntax::object::String<'_>>(b"Name").unwrap().as_bytes(), b"Hidden");
let config = properties.get::<Dict<'_>>(b"D").unwrap();
if hidden {
+8 -8
View File
@@ -47,9 +47,9 @@ fn rtl() -> ParaAttrs {
/// The ActualText of every marked-content span on the first page.
fn actual_texts(bytes: &[u8]) -> Vec<String> {
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<String> {
let TypedInstruction::BeginMarkedContentWithProperties(op) = op else { continue };
let Object::Dict(props) = op.1 else { continue };
let props: &Dict<'_> = props;
let Some(s) = props.get::<hayro_syntax::object::String>(b"ActualText") else { continue };
let Some(s) = props.get::<craft_pdf::syntax::object::String>(b"ActualText") else { continue };
let units: Vec<u16> = 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<kurbo::Point> {
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();
+14 -11
View File
@@ -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<String> {
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<SoftMask<'_>>) {}
@@ -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<u8>)> {
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<u8> {
/// Each image XObject's /Interpolate value (`None` when absent).
fn interpolate_flags(bytes: &[u8]) -> Vec<Option<bool>> {
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<kurbo::Rect>) {
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());
+5 -5
View File
@@ -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::<hayro_syntax::object::String<'_>>(b"OutputConditionIdentifier").unwrap().as_bytes(), b"FOGRA39");
assert_eq!(oi.get::<hayro_syntax::object::String<'_>>(b"RegistryName").unwrap().as_bytes(), b"http://www.color.org");
assert_eq!(oi.get::<craft_pdf::syntax::object::String<'_>>(b"OutputConditionIdentifier").unwrap().as_bytes(), b"FOGRA39");
assert_eq!(oi.get::<craft_pdf::syntax::object::String<'_>>(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::<i32>(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::<Dict<'_>>(xref.root_id()).unwrap();
let intents = catalog.get::<hayro_syntax::object::Array<'_>>(b"OutputIntents").unwrap();
let intents = catalog.get::<craft_pdf::syntax::object::Array<'_>>(b"OutputIntents").unwrap();
intents.iter::<Dict<'_>>().find(|oi| oi.get::<Name<'_>>(b"S").is_some_and(|s| s.as_ref() == b"GTS_PDFX")).unwrap()
}
+4 -4
View File
@@ -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() });
+4 -4
View File
@@ -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";
@@ -93,7 +93,7 @@ fn drawn_content(bytes: Vec<u8>) -> (Vec<DrawnImage>, 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<ImageObject> {
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())
+64 -73
View File
@@ -26,40 +26,43 @@ fn style_names(styles: &StyleSheet) -> HashMap<String, String> {
}
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<String, String>,
warned_objects: &mut bool,
notes_done: &mut HashSet<u32>,
note_bytes: &mut usize,
warnings: &mut Vec<String>,
) {
struct ImportContext<'a> {
doc: &'a craft_wordmodel::Document,
names: &'a HashMap<String, String>,
warned_objects: bool,
notes_done: HashSet<u32>,
note_bytes: usize,
warnings: Vec<String>,
}
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<CharFormat> = 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<Block>],
st: &mut Story,
names: &HashMap<String, String>,
warned_objects: &mut bool,
notes_done: &mut HashSet<u32>,
note_bytes: &mut usize,
warnings: &mut Vec<String>,
) {
fn append_blocks(ctx: &mut ImportContext<'_>, blocks: &[std::sync::Arc<Block>], 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<Vec<String>> = 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<Imported, ImportError> {
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<String> = story.paras.iter().map(|p| p.style.clone()).collect();
let used_c: HashSet<String> = story.runs().map(|(_, f)| f.style.clone()).collect();
@@ -314,7 +305,7 @@ pub fn import(bytes: &[u8]) -> Result<Imported, ImportError> {
};
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<String>, message: String) {