Add proper error conversion

This commit is contained in:
Laurenz Stampfl 2024-12-15 23:17:09 +01:00
parent 6761cbc622
commit 5780432039
5 changed files with 296 additions and 177 deletions

View File

@ -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 crate::{paint, PdfOptions};
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use ecow::EcoString; use ecow::EcoString;
@ -12,7 +12,9 @@ use krilla::{PageSettings, SerializeSettings, SvgSettings};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use krilla::error::KrillaError;
use krilla::geom::Rect; use krilla::geom::Rect;
use krilla::validation::ValidationError;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::Datetime; use typst_library::foundations::Datetime;
use typst_library::layout::{ use typst_library::layout::{
@ -150,7 +152,8 @@ impl krilla::font::Glyph for PdfGlyph {
} }
pub struct GlobalContext { pub struct GlobalContext {
fonts: HashMap<Font, krilla::font::Font>, fonts_forward: HashMap<Font, krilla::font::Font>,
fonts_backward: HashMap<krilla::font::Font, Font>,
// Note: In theory, the same image can have multiple spans // Note: In theory, the same image can have multiple spans
// if it appears in the document multiple times. We just store the // if it appears in the document multiple times. We just store the
// first appearance, though. // first appearance, though.
@ -161,7 +164,8 @@ pub struct GlobalContext {
impl GlobalContext { impl GlobalContext {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
fonts: HashMap::new(), fonts_forward: HashMap::new(),
fonts_backward: HashMap::new(),
image_spans: HashMap::new(), image_spans: HashMap::new(),
languages: BTreeMap::new(), languages: BTreeMap::new(),
} }
@ -279,7 +283,111 @@ pub fn pdf(
document.set_metadata(metadata); 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<krilla::metadata::DateTime> { fn krilla_date(datetime: Datetime, tz: bool) -> Option<krilla::metadata::DateTime> {
@ -442,15 +550,24 @@ pub fn handle_text(
surface: &mut Surface, surface: &mut Surface,
gc: &mut GlobalContext, gc: &mut GlobalContext,
) -> SourceResult<()> { ) -> SourceResult<()> {
let font = gc let typst_font = t.font.clone();
.fonts
.entry(t.font.clone()) let krilla_font = if let Some(font) = gc.fonts_forward.get(&typst_font) {
.or_insert_with(|| { font.clone()
krilla::font::Font::new(Arc::new(t.font.data().clone()), t.font.index(), true) } else {
// TODO: DOn't unwrap let font = match krilla::font::Font::new(Arc::new(typst_font.data().clone()), typst_font.index(), true) {
.unwrap() None => {
}) let font_str = font_to_str(&typst_font);
.clone(); 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(); *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), krilla::geom::Point::from_xy(0.0, 0.0),
fill, fill,
&glyphs, &glyphs,
font.clone(), krilla_font.clone(),
text, text,
size.to_f32(), size.to_f32(),
GlyphUnits::Normalized, GlyphUnits::Normalized,
@ -491,7 +608,7 @@ pub fn handle_text(
krilla::geom::Point::from_xy(0.0, 0.0), krilla::geom::Point::from_xy(0.0, 0.0),
stroke, stroke,
&glyphs, &glyphs,
font.clone(), krilla_font.clone(),
text, text,
size.to_f32(), size.to_f32(),
GlyphUnits::Normalized, GlyphUnits::Normalized,

View File

@ -4,14 +4,11 @@
mod image; mod image;
mod krilla; mod krilla;
mod paint; mod paint;
mod primitive; mod util;
use std::fmt::Debug; use typst_library::diag::{SourceResult};
use std::hash::Hash;
use std::ops::{Deref, DerefMut};
use typst_library::diag::{bail, SourceResult, StrResult};
use typst_library::foundations::{Datetime, Smart}; 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. /// Export a document into a PDF file.
/// ///

View File

@ -1,15 +1,12 @@
//! Convert paint types from typst to krilla. //! Convert paint types from typst to krilla.
use crate::krilla::{process_frame, FrameContext, GlobalContext, Transforms}; 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::geom::NormalizedF32;
use krilla::page::{NumberingStyle, PageLabel};
use krilla::paint::SpreadMethod; use krilla::paint::SpreadMethod;
use krilla::surface::Surface; use krilla::surface::Surface;
use std::num::NonZeroUsize;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Transform}; use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Transform};
use typst_library::model::Numbering;
use typst_library::visualize::{ use typst_library::visualize::{
Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, Pattern, Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, Pattern,
RatioOrAngle, RelativeTo, WeightedColor, RatioOrAngle, RelativeTo, WeightedColor,
@ -86,61 +83,6 @@ fn paint(
} }
} }
pub(crate) trait PageLabelExt {
fn generate(numbering: &Numbering, number: usize) -> Option<PageLabel>;
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<PageLabel> {
{
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( pub(crate) fn convert_pattern(
gc: &mut GlobalContext, gc: &mut GlobalContext,
pattern: &Pattern, pattern: &Pattern,
@ -293,7 +235,6 @@ fn convert_gradient(
// If we have a hue index or are using Oklab, we will create several // If we have a hue index or are using Oklab, we will create several
// stops in-between to make the gradient smoother without interpolation // stops in-between to make the gradient smoother without interpolation
// issues with native color spaces. // issues with native color spaces.
let mut last_c = first.0;
if gradient.space().hue_index().is_some() { if gradient.space().hue_index().is_some() {
for i in 0..=32 { for i in 0..=32 {
let t = i as f64 / 32.0; let t = i as f64 / 32.0;
@ -302,7 +243,6 @@ fn convert_gradient(
let c = gradient.sample(RatioOrAngle::Ratio(real_t)); let c = gradient.sample(RatioOrAngle::Ratio(real_t));
add_single(&c, real_t); add_single(&c, real_t);
last_c = c;
} }
} }

View File

@ -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
}
}

View File

@ -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<PageLabel>;
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<PageLabel> {
{
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)
}