mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Route PDF parsing and raster import through craft-libs
This commit is contained in:
21 files changed
+207
-210
No files matched your search
Generated
+4
-4
@@ -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
@@ -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.
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user