From 5780432039493b70a6882199d075db22446be5ec Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:17:09 +0100 Subject: [PATCH] Add proper error conversion --- crates/typst-pdf/src/krilla.rs | 147 ++++++++++++++++++++++++--- crates/typst-pdf/src/lib.rs | 9 +- crates/typst-pdf/src/paint.rs | 62 +----------- crates/typst-pdf/src/primitive.rs | 95 ------------------ crates/typst-pdf/src/util.rs | 160 ++++++++++++++++++++++++++++++ 5 files changed, 296 insertions(+), 177 deletions(-) delete mode 100644 crates/typst-pdf/src/primitive.rs create mode 100644 crates/typst-pdf/src/util.rs diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs index 952a92528..14efb135b 100644 --- a/crates/typst-pdf/src/krilla.rs +++ b/crates/typst-pdf/src/krilla.rs @@ -1,4 +1,4 @@ -use crate::primitive::{AbsExt, PointExt, SizeExt, TransformExt}; +use crate::util::{font_to_str, AbsExt, PointExt, SizeExt, TransformExt}; use crate::{paint, PdfOptions}; use bytemuck::TransparentWrapper; use ecow::EcoString; @@ -12,7 +12,9 @@ use krilla::{PageSettings, SerializeSettings, SvgSettings}; use std::collections::{BTreeMap, HashMap}; use std::ops::Range; use std::sync::Arc; +use krilla::error::KrillaError; use krilla::geom::Rect; +use krilla::validation::ValidationError; use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::Datetime; use typst_library::layout::{ @@ -150,7 +152,8 @@ impl krilla::font::Glyph for PdfGlyph { } pub struct GlobalContext { - fonts: HashMap, + fonts_forward: HashMap, + fonts_backward: HashMap, // Note: In theory, the same image can have multiple spans // if it appears in the document multiple times. We just store the // first appearance, though. @@ -161,7 +164,8 @@ pub struct GlobalContext { impl GlobalContext { pub fn new() -> Self { Self { - fonts: HashMap::new(), + fonts_forward: HashMap::new(), + fonts_backward: HashMap::new(), image_spans: HashMap::new(), languages: BTreeMap::new(), } @@ -279,7 +283,111 @@ pub fn pdf( document.set_metadata(metadata); - Ok(document.finish().unwrap()) + match document.finish() { + Ok(r) => Ok(r), + Err(e) => match e { + KrillaError::FontError(f, s) => { + let font_str = font_to_str(gc.fonts_backward.get(&f).unwrap()); + bail!(Span::detached(), "failed to process font {font_str} ({s})"); + } + KrillaError::UserError(u) => { + // This is an error which indicates misuse on the typst-pdf side. + bail!(Span::detached(), "internal error ({u})"; hint: "please report this as a bug") + } + KrillaError::ValidationError(ve) => { + // We can only produce 1 error, so just take the first one. + let prefix = "validated export failed:"; + match &ve[0] { + ValidationError::TooLongString => { + bail!(Span::detached(), "{prefix} a PDF string longer than 32767 characters"; + hint: "make sure title and author names are short enough"); + } + // Should in theory never occur, as krilla always trims font names + ValidationError::TooLongName => { + bail!(Span::detached(), "{prefix} a PDF name longer than 127 characters"; + hint: "perhaps a font name is too long"); + } + ValidationError::TooLongArray => { + bail!(Span::detached(), "{prefix} a PDF array longer than 8191 elements"; + hint: "this can happen if you have a very long text in a single line"); + } + ValidationError::TooLongDictionary => { + bail!(Span::detached(), "{prefix} a PDF dictionary had more than 4095 entries"; + hint: "try reducing the complexity of your document"); + } + ValidationError::TooLargeFloat => { + bail!(Span::detached(), "{prefix} a PDF float was larger than the allowed limit"; + hint: "try exporting using a higher PDF version"); + } + ValidationError::TooManyIndirectObjects => { + bail!(Span::detached(), "{prefix} the PDF has too many indirect objects"; + hint: "reduce the size of your document"); + } + ValidationError::TooHighQNestingLevel => { + bail!(Span::detached(), "{prefix} the PDF has too high q nesting"; + hint: "reduce the number of nested containers"); + } + ValidationError::ContainsPostScript => { + bail!(Span::detached(), "{prefix} the PDF contains PostScript code"; + hint: "sweep gradients are not supported in this PDF standard"); + } + ValidationError::MissingCMYKProfile => { + bail!(Span::detached(), "{prefix} the PDF is missing a CMYK profile"; + hint: "CMYK colors are not yet supported in this export mode"); + } + ValidationError::ContainsNotDefGlyph => { + bail!(Span::detached(), "{prefix} the PDF contains the .notdef glyph"; + hint: "ensure all text can be displayed using a font"); + } + ValidationError::InvalidCodepointMapping(_, _) => { + bail!(Span::detached(), "{prefix} the PDF contains the disallowed codepoints"; + hint: "make sure you don't use the Unicode characters 0x0, 0xFEFF or 0xFFFE"); + } + ValidationError::UnicodePrivateArea(_, _) => { + bail!(Span::detached(), "{prefix} the PDF contains characters from the Unicode private area"; + hint: "remove the text containing codepoints from the Unicode private area"); + } + ValidationError::Transparency => { + bail!(Span::detached(), "{prefix} document contains transparency"; + hint: "remove any transparency in your document and your SVGs"; + hint: "export using a different standard that supports transparency" + ); + } + // The below errors cannot occur yet, only once Typst supports PDF/A and PDF/UA. + ValidationError::MissingAnnotationAltText => { + bail!(Span::detached(), "{prefix} missing annotation alt text"; + hint: "please report this as a bug"); + } + ValidationError::MissingAltText => { + bail!(Span::detached(), "{prefix} missing alt text"; + hint: "make sure your images and formulas have alt text"); + } + ValidationError::NoDocumentLanguage => { + bail!(Span::detached(), "{prefix} missing document language"; + hint: "set the language of the document"); + } + // Needs to be set by Typst. + ValidationError::MissingHeadingTitle => { + bail!(Span::detached(), "{prefix} missing heading title"; + hint: "please report this as a bug"); + } + // Needs to be set by Typst. + ValidationError::MissingDocumentOutline => { + bail!(Span::detached(), "{prefix} missing document outline"; + hint: "please report this as a bug"); + } + ValidationError::NoDocumentTitle => { + bail!(Span::detached(), "{prefix} missing document title"; + hint: "set the title of the document"); + } + } + } + KrillaError::ImageError(i) => { + let span = gc.image_spans.get(&i).unwrap(); + bail!(*span, "failed to process image"); + } + } + } } fn krilla_date(datetime: Datetime, tz: bool) -> Option { @@ -442,15 +550,24 @@ pub fn handle_text( surface: &mut Surface, gc: &mut GlobalContext, ) -> SourceResult<()> { - let font = gc - .fonts - .entry(t.font.clone()) - .or_insert_with(|| { - krilla::font::Font::new(Arc::new(t.font.data().clone()), t.font.index(), true) - // TODO: DOn't unwrap - .unwrap() - }) - .clone(); + let typst_font = t.font.clone(); + + let krilla_font = if let Some(font) = gc.fonts_forward.get(&typst_font) { + font.clone() + } else { + let font = match krilla::font::Font::new(Arc::new(typst_font.data().clone()), typst_font.index(), true) { + None => { + let font_str = font_to_str(&typst_font); + bail!(Span::detached(), "failed to process font {font_str}"); + } + Some(f) => f + }; + + gc.fonts_forward.insert(typst_font.clone(), font.clone()); + gc.fonts_backward.insert(font.clone(), typst_font.clone()); + + font + }; *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); @@ -473,7 +590,7 @@ pub fn handle_text( krilla::geom::Point::from_xy(0.0, 0.0), fill, &glyphs, - font.clone(), + krilla_font.clone(), text, size.to_f32(), GlyphUnits::Normalized, @@ -491,7 +608,7 @@ pub fn handle_text( krilla::geom::Point::from_xy(0.0, 0.0), stroke, &glyphs, - font.clone(), + krilla_font.clone(), text, size.to_f32(), GlyphUnits::Normalized, diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 3596bf73e..5b3972f57 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -4,14 +4,11 @@ mod image; mod krilla; mod paint; -mod primitive; +mod util; -use std::fmt::Debug; -use std::hash::Hash; -use std::ops::{Deref, DerefMut}; -use typst_library::diag::{bail, SourceResult, StrResult}; +use typst_library::diag::{SourceResult}; use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; +use typst_library::layout::{PageRanges, PagedDocument}; /// Export a document into a PDF file. /// diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs index 1e7a16b04..1f549175b 100644 --- a/crates/typst-pdf/src/paint.rs +++ b/crates/typst-pdf/src/paint.rs @@ -1,15 +1,12 @@ //! Convert paint types from typst to krilla. use crate::krilla::{process_frame, FrameContext, GlobalContext, Transforms}; -use crate::primitive::{AbsExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; +use crate::util::{AbsExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; use krilla::geom::NormalizedF32; -use krilla::page::{NumberingStyle, PageLabel}; use krilla::paint::SpreadMethod; use krilla::surface::Surface; -use std::num::NonZeroUsize; use typst_library::diag::SourceResult; use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Transform}; -use typst_library::model::Numbering; use typst_library::visualize::{ Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, Pattern, RatioOrAngle, RelativeTo, WeightedColor, @@ -86,61 +83,6 @@ fn paint( } } -pub(crate) trait PageLabelExt { - fn generate(numbering: &Numbering, number: usize) -> Option; - fn arabic(number: usize) -> PageLabel; -} - -impl PageLabelExt for PageLabel { - /// Create a new `PageLabel` from a `Numbering` applied to a page - /// number. - fn generate(numbering: &Numbering, number: usize) -> Option { - { - let Numbering::Pattern(pat) = numbering else { - return None; - }; - - let (prefix, kind) = pat.pieces.first()?; - - // If there is a suffix, we cannot use the common style optimisation, - // since PDF does not provide a suffix field. - let style = if pat.suffix.is_empty() { - use krilla::page::NumberingStyle as Style; - use typst_library::model::NumberingKind as Kind; - match kind { - Kind::Arabic => Some(Style::Arabic), - Kind::LowerRoman => Some(Style::LowerRoman), - Kind::UpperRoman => Some(Style::UpperRoman), - Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), - Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), - _ => None, - } - } else { - None - }; - - // Prefix and offset depend on the style: If it is supported by the PDF - // spec, we use the given prefix and an offset. Otherwise, everything - // goes into prefix. - let prefix = if style.is_none() { - Some(pat.apply(&[number])) - } else { - (!prefix.is_empty()).then(|| prefix.clone()) - }; - - let offset = style.and(NonZeroUsize::new(number)); - Some(PageLabel::new(style, prefix.map(|s| s.to_string()), offset)) - } - } - - /// Creates an arabic page label with the specified page number. - /// For example, this will display page label `11` when given the page - /// number 11. - fn arabic(number: usize) -> PageLabel { - PageLabel::new(Some(NumberingStyle::Arabic), None, NonZeroUsize::new(number)) - } -} - pub(crate) fn convert_pattern( gc: &mut GlobalContext, pattern: &Pattern, @@ -293,7 +235,6 @@ fn convert_gradient( // If we have a hue index or are using Oklab, we will create several // stops in-between to make the gradient smoother without interpolation // issues with native color spaces. - let mut last_c = first.0; if gradient.space().hue_index().is_some() { for i in 0..=32 { let t = i as f64 / 32.0; @@ -302,7 +243,6 @@ fn convert_gradient( let c = gradient.sample(RatioOrAngle::Ratio(real_t)); add_single(&c, real_t); - last_c = c; } } diff --git a/crates/typst-pdf/src/primitive.rs b/crates/typst-pdf/src/primitive.rs deleted file mode 100644 index 3ccc51845..000000000 --- a/crates/typst-pdf/src/primitive.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Convert basic primitive types from typst to krilla. - -use typst_library::layout::{Abs, Point, Size, Transform}; -use typst_library::visualize::{FillRule, LineCap, LineJoin}; - - -pub(crate) trait SizeExt { - fn as_krilla(&self) -> krilla::geom::Size; -} - -impl SizeExt for Size { - fn as_krilla(&self) -> krilla::geom::Size { - krilla::geom::Size::from_wh(self.x.to_f32(), self.y.to_f32()).unwrap() - } -} - -pub(crate) trait PointExt { - fn as_krilla(&self) -> krilla::geom::Point; -} - -impl PointExt for Point { - fn as_krilla(&self) -> krilla::geom::Point { - krilla::geom::Point::from_xy(self.x.to_f32(), self.y.to_f32()) - } -} - -pub(crate) trait LineCapExt { - fn as_krilla(&self) -> krilla::path::LineCap; -} - -impl LineCapExt for LineCap { - fn as_krilla(&self) -> krilla::path::LineCap { - match self { - LineCap::Butt => krilla::path::LineCap::Butt, - LineCap::Round => krilla::path::LineCap::Round, - LineCap::Square => krilla::path::LineCap::Square, - } - } -} - -pub(crate) trait LineJoinExt { - fn as_krilla(&self) -> krilla::path::LineJoin; -} - -impl LineJoinExt for LineJoin { - fn as_krilla(&self) -> krilla::path::LineJoin { - match self { - LineJoin::Miter => krilla::path::LineJoin::Miter, - LineJoin::Round => krilla::path::LineJoin::Round, - LineJoin::Bevel => krilla::path::LineJoin::Bevel, - } - } -} - -pub(crate) trait TransformExt { - fn as_krilla(&self) -> krilla::geom::Transform; -} - -impl TransformExt for Transform { - fn as_krilla(&self) -> krilla::geom::Transform { - krilla::geom::Transform::from_row( - self.sx.get() as f32, - self.ky.get() as f32, - self.kx.get() as f32, - self.sy.get() as f32, - self.tx.to_f32(), - self.ty.to_f32(), - ) - } -} - -pub(crate) trait FillRuleExt { - fn as_krilla(&self) -> krilla::path::FillRule; -} - -impl FillRuleExt for FillRule { - fn as_krilla(&self) -> krilla::path::FillRule { - match self { - FillRule::NonZero => krilla::path::FillRule::NonZero, - FillRule::EvenOdd => krilla::path::FillRule::EvenOdd, - } - } -} - -/// Additional methods for [`Abs`]. -pub(crate) trait AbsExt { - /// Convert an to a number of points. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} diff --git a/crates/typst-pdf/src/util.rs b/crates/typst-pdf/src/util.rs new file mode 100644 index 000000000..64915d1b2 --- /dev/null +++ b/crates/typst-pdf/src/util.rs @@ -0,0 +1,160 @@ +//! Convert basic primitive types from typst to krilla. + +use std::num::NonZeroUsize; +use krilla::page::{NumberingStyle, PageLabel}; +use typst_library::layout::{Abs, Point, Size, Transform}; +use typst_library::model::Numbering; +use typst_library::text::Font; +use typst_library::visualize::{FillRule, LineCap, LineJoin}; + + +pub(crate) trait SizeExt { + fn as_krilla(&self) -> krilla::geom::Size; +} + +impl SizeExt for Size { + fn as_krilla(&self) -> krilla::geom::Size { + krilla::geom::Size::from_wh(self.x.to_f32(), self.y.to_f32()).unwrap() + } +} + +pub(crate) trait PointExt { + fn as_krilla(&self) -> krilla::geom::Point; +} + +impl PointExt for Point { + fn as_krilla(&self) -> krilla::geom::Point { + krilla::geom::Point::from_xy(self.x.to_f32(), self.y.to_f32()) + } +} + +pub(crate) trait LineCapExt { + fn as_krilla(&self) -> krilla::path::LineCap; +} + +impl LineCapExt for LineCap { + fn as_krilla(&self) -> krilla::path::LineCap { + match self { + LineCap::Butt => krilla::path::LineCap::Butt, + LineCap::Round => krilla::path::LineCap::Round, + LineCap::Square => krilla::path::LineCap::Square, + } + } +} + +pub(crate) trait LineJoinExt { + fn as_krilla(&self) -> krilla::path::LineJoin; +} + +impl LineJoinExt for LineJoin { + fn as_krilla(&self) -> krilla::path::LineJoin { + match self { + LineJoin::Miter => krilla::path::LineJoin::Miter, + LineJoin::Round => krilla::path::LineJoin::Round, + LineJoin::Bevel => krilla::path::LineJoin::Bevel, + } + } +} + +pub(crate) trait TransformExt { + fn as_krilla(&self) -> krilla::geom::Transform; +} + +impl TransformExt for Transform { + fn as_krilla(&self) -> krilla::geom::Transform { + krilla::geom::Transform::from_row( + self.sx.get() as f32, + self.ky.get() as f32, + self.kx.get() as f32, + self.sy.get() as f32, + self.tx.to_f32(), + self.ty.to_f32(), + ) + } +} + +pub(crate) trait FillRuleExt { + fn as_krilla(&self) -> krilla::path::FillRule; +} + +impl FillRuleExt for FillRule { + fn as_krilla(&self) -> krilla::path::FillRule { + match self { + FillRule::NonZero => krilla::path::FillRule::NonZero, + FillRule::EvenOdd => krilla::path::FillRule::EvenOdd, + } + } +} + +/// Additional methods for [`Abs`]. +pub(crate) trait AbsExt { + /// Convert an to a number of points. + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +pub(crate) trait PageLabelExt { + fn generate(numbering: &Numbering, number: usize) -> Option; + fn arabic(number: usize) -> PageLabel; +} + +impl PageLabelExt for PageLabel { + /// Create a new `PageLabel` from a `Numbering` applied to a page + /// number. + fn generate(numbering: &Numbering, number: usize) -> Option { + { + let Numbering::Pattern(pat) = numbering else { + return None; + }; + + let (prefix, kind) = pat.pieces.first()?; + + // If there is a suffix, we cannot use the common style optimisation, + // since PDF does not provide a suffix field. + let style = if pat.suffix.is_empty() { + use krilla::page::NumberingStyle as Style; + use typst_library::model::NumberingKind as Kind; + match kind { + Kind::Arabic => Some(Style::Arabic), + Kind::LowerRoman => Some(Style::LowerRoman), + Kind::UpperRoman => Some(Style::UpperRoman), + Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), + Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), + _ => None, + } + } else { + None + }; + + // Prefix and offset depend on the style: If it is supported by the PDF + // spec, we use the given prefix and an offset. Otherwise, everything + // goes into prefix. + let prefix = if style.is_none() { + Some(pat.apply(&[number])) + } else { + (!prefix.is_empty()).then(|| prefix.clone()) + }; + + let offset = style.and(NonZeroUsize::new(number)); + Some(PageLabel::new(style, prefix.map(|s| s.to_string()), offset)) + } + } + + /// Creates an arabic page label with the specified page number. + /// For example, this will display page label `11` when given the page + /// number 11. + fn arabic(number: usize) -> PageLabel { + PageLabel::new(Some(NumberingStyle::Arabic), None, NonZeroUsize::new(number)) + } +} + +pub(crate) fn font_to_str(font: &Font) -> String { + let font_family = &font.info().family; + let font_variant = font.info().variant; + format!("{} ({:?})", font_family, font_variant) +} \ No newline at end of file