First version of better error support

This commit is contained in:
Laurenz Stampfl 2025-03-11 10:49:13 +01:00
parent 84dcaaeae2
commit ccec0dedbf
7 changed files with 147 additions and 116 deletions

1
Cargo.lock generated
View File

@ -1345,7 +1345,6 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/LaurenzV/krilla?rev=e7006f2#e7006f2f0ba598bbe426e8d63306fb2e007c4988"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",

View File

@ -70,7 +70,7 @@ if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla", rev="e7006f2", features = ["svg", "raster-images", "comemo", "rayon"] } krilla = { path = "../krilla/crates/krilla", features = ["svg", "raster-images", "comemo", "rayon"] }
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"

View File

@ -1,5 +1,4 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use ecow::EcoVec;
use krilla::annotation::Annotation; use krilla::annotation::Annotation;
use krilla::destination::{NamedDestination, XyzDestination}; use krilla::destination::{NamedDestination, XyzDestination};
use krilla::error::KrillaError; use krilla::error::KrillaError;
@ -7,7 +6,9 @@ use krilla::page::PageLabel;
use krilla::path::PathBuilder; use krilla::path::PathBuilder;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::{Configuration, Document, PageSettings, SerializeSettings, ValidationError}; use krilla::{Configuration, Document, PageSettings, SerializeSettings, ValidationError};
use typst_library::diag::{bail, SourceResult}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::num::NonZeroU64;
use typst_library::diag::{bail, error, SourceResult};
use typst_library::foundations::NativeElement; use typst_library::foundations::NativeElement;
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::layout::{ use typst_library::layout::{
@ -209,7 +210,8 @@ pub(crate) struct GlobalContext<'a> {
// 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.
pub(crate) image_spans: HashMap<krilla::image::Image, Span>, pub(crate) image_to_spans: HashMap<krilla::image::Image, Span>,
pub(crate) image_spans: HashSet<Span>,
pub(crate) document: &'a PagedDocument, pub(crate) document: &'a PagedDocument,
pub(crate) options: &'a PdfOptions<'a>, pub(crate) options: &'a PdfOptions<'a>,
/// Mapping between locations in the document and named destinations. /// Mapping between locations in the document and named destinations.
@ -230,7 +232,8 @@ impl<'a> GlobalContext<'a> {
document, document,
options, options,
loc_to_named, loc_to_named,
image_spans: HashMap::new(), image_to_spans: HashMap::new(),
image_spans: HashSet::new(),
languages: BTreeMap::new(), languages: BTreeMap::new(),
} }
} }
@ -258,7 +261,7 @@ pub(crate) fn handle_frame(
if let Some(fill) = fill { if let Some(fill) = fill {
let shape = Geometry::Rect(frame.size()).filled(fill); let shape = Geometry::Rect(frame.size()).filled(fill);
handle_shape(fc, &shape, surface, gc)?; handle_shape(fc, &shape, surface, gc, Span::detached())?;
} }
for (point, item) in frame.items() { for (point, item) in frame.items() {
@ -268,7 +271,7 @@ pub(crate) fn handle_frame(
match item { match item {
FrameItem::Group(g) => handle_group(fc, g, surface, gc)?, FrameItem::Group(g) => handle_group(fc, g, surface, gc)?,
FrameItem::Text(t) => handle_text(fc, t, surface, gc)?, FrameItem::Text(t) => handle_text(fc, t, surface, gc)?,
FrameItem::Shape(s, _) => handle_shape(fc, s, surface, gc)?, FrameItem::Shape(s, span) => handle_shape(fc, s, surface, gc, *span)?,
FrameItem::Image(image, size, span) => { FrameItem::Image(image, size, span) => {
handle_image(gc, fc, image, *size, surface, *span)? handle_image(gc, fc, image, *size, surface, *span)?
} }
@ -331,7 +334,10 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
Err(e) => match e { Err(e) => match e {
KrillaError::FontError(f, s) => { KrillaError::FontError(f, s) => {
let font_str = display_font(gc.fonts_backward.get(&f).unwrap()); let font_str = display_font(gc.fonts_backward.get(&f).unwrap());
bail!(Span::detached(), "failed to process font {font_str} ({s})"); bail!(Span::detached(), "failed to process font {font_str} ({s})";
hint: "make sure the font is valid";
hint: "this could also be a bug in the Typst compiler"
);
} }
KrillaError::UserError(u) => { KrillaError::UserError(u) => {
// This is an error which indicates misuse on the typst-pdf side. // This is an error which indicates misuse on the typst-pdf side.
@ -340,125 +346,142 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
KrillaError::ValidationError(ve) => { KrillaError::ValidationError(ve) => {
// We can only produce 1 error, so just take the first one. // We can only produce 1 error, so just take the first one.
let prefix = let prefix =
format!("validated export for {} failed:", validator.as_str()); format!("validated export with {} failed:", validator.as_str());
match &ve[0] { let get_span = |loc: Option<krilla::surface::Location>| {
loc.map(|l| Span::from_raw(NonZeroU64::new(l).unwrap()))
.unwrap_or(Span::detached())
};
let mut errors = ve.iter().map(|e| {
match e {
ValidationError::TooLongString => { ValidationError::TooLongString => {
bail!(Span::detached(), "{prefix} a PDF string longer \ error!(Span::detached(), "{prefix} a PDF string is longer \
than 32767 characters"; than 32767 characters";
hint: "make sure title and author names are short enough"); hint: "ensure title and author names are short enough")
} }
// Should in theory never occur, as krilla always trims font names // Should in theory never occur, as krilla always trims font names
ValidationError::TooLongName => { ValidationError::TooLongName => {
bail!(Span::detached(), "{prefix} a PDF name longer than 127 characters"; error!(Span::detached(), "{prefix} a PDF name is longer than 127 characters";
hint: "perhaps a font name is too long"); hint: "perhaps a font name is too long")
} }
ValidationError::TooLongArray => { ValidationError::TooLongArray => {
bail!(Span::detached(), "{prefix} a PDF array longer than 8191 elements"; error!(Span::detached(), "{prefix} a PDF array is longer than 8191 elements";
hint: "this can happen if you have a very long text in a single line"); hint: "this can happen if you have a very long text in a single line")
} }
ValidationError::TooLongDictionary => { ValidationError::TooLongDictionary => {
bail!(Span::detached(), "{prefix} a PDF dictionary had \ error!(Span::detached(), "{prefix} a PDF dictionary has \
more than 4095 entries"; more than 4095 entries";
hint: "try reducing the complexity of your document"); hint: "try reducing the complexity of your document")
} }
ValidationError::TooLargeFloat => { ValidationError::TooLargeFloat => {
bail!(Span::detached(), "{prefix} a PDF float was larger than \ error!(Span::detached(), "{prefix} a PDF float number is larger than \
the allowed limit"; the allowed limit";
hint: "try exporting using a higher PDF version"); hint: "try exporting using a higher PDF version")
} }
ValidationError::TooManyIndirectObjects => { ValidationError::TooManyIndirectObjects => {
bail!(Span::detached(), "{prefix} the PDF has too many indirect objects"; error!(Span::detached(), "{prefix} the PDF has too many indirect objects";
hint: "reduce the size of your document"); hint: "reduce the size of your document")
} }
// Can only occur if we have 27+ nested clip paths // Can only occur if we have 27+ nested clip paths
ValidationError::TooHighQNestingLevel => { ValidationError::TooHighQNestingLevel => {
bail!(Span::detached(), "{prefix} the PDF has too high q nesting"; error!(Span::detached(), "{prefix} the PDF has too high q nesting";
hint: "reduce the number of nested containers"); hint: "reduce the number of nested containers")
} }
ValidationError::ContainsPostScript => { ValidationError::ContainsPostScript(loc) => {
bail!(Span::detached(), "{prefix} the PDF contains PostScript code"; error!(get_span(*loc), "{prefix} the PDF contains PostScript code";
hint: "sweep gradients are not supported in this PDF standard"); hint: "conic gradients are not supported in this PDF standard")
} }
ValidationError::MissingCMYKProfile => { ValidationError::MissingCMYKProfile => {
bail!(Span::detached(), "{prefix} the PDF is missing a CMYK profile"; error!(Span::detached(), "{prefix} the PDF is missing a CMYK profile";
hint: "CMYK colors are not yet supported in this export mode"); hint: "CMYK colors are not yet supported in this export mode")
} }
ValidationError::ContainsNotDefGlyph => { ValidationError::ContainsNotDefGlyph(f, loc, text) => {
bail!(Span::detached(), "{prefix} the PDF contains the .notdef glyph"; let span = get_span(*loc);
hint: "ensure all text can be displayed using an available font"); let font_str = display_font(gc.fonts_backward.get(&f).unwrap());
error!(span, "{prefix} the text '{text}' cannot be displayed using {font_str}";
hint: "try using a different font"
)
} }
ValidationError::InvalidCodepointMapping(_, _) => { ValidationError::InvalidCodepointMapping(_, _, _, loc) => {
bail!(Span::detached(), "{prefix} the PDF contains \ error!(get_span(*loc), "{prefix} the PDF contains \
disallowed codepoints or is missing codepoint mappings"; disallowed codepoints or is missing codepoint mappings";
hint: "make sure to not use the unicode characters 0x0, \ hint: "make sure to not use the unicode characters 0x0, \
0xFEFF or 0xFFFE"; 0xFEFF or 0xFFFE";
hint: "for complex scripts like indic or arabic, it might \ hint: "for complex scripts like indic or arabic, it might \
not be possible to produce a compliant document"); not be possible to produce a compliant document")
} }
ValidationError::UnicodePrivateArea(_, _) => { ValidationError::UnicodePrivateArea(_, _, _, loc) => {
bail!(Span::detached(), "{prefix} the PDF contains characters from the \ error!(get_span(*loc), "{prefix} the PDF contains characters from the \
Unicode private area"; Unicode private area";
hint: "remove the text containing codepoints \ hint: "remove the text containing codepoints \
from the Unicode private area"); from the Unicode private area")
} }
ValidationError::Transparency => { ValidationError::Transparency(loc) => {
bail!(Span::detached(), "{prefix} document contains transparency"; error!(get_span(*loc), "{prefix} document contains transparency";
hint: "remove any transparency from your \ hint: "remove any transparency from your \
document (e.g. fills with opacity)"; document (e.g. fills with opacity)";
hint: "you might have to convert certain SVGs into a bitmap image if \ hint: "you might have to convert certain SVGs into a bitmap image if \
they contain transparency"; they contain transparency";
hint: "export using a different standard that supports transparency" hint: "export using a different standard that supports transparency"
); )
} }
ValidationError::ImageInterpolation => { ValidationError::ImageInterpolation(loc) => {
bail!(Span::detached(), "{prefix} document contains an image with smooth interpolation"; error!(get_span(*loc), "{prefix} the image has smooth interpolation";
hint: "such images are not supported in this export mode" hint: "such images are not supported in this export mode"
); )
} }
ValidationError::EmbeddedFile(_) => { ValidationError::EmbeddedFile(_) => {
bail!(Span::detached(), "{prefix} document contains an embedded file"; error!(Span::detached(), "{prefix} document contains an embedded file";
hint: "embedded files are not supported in this export mode" hint: "embedded files are not supported in this export mode"
); )
} }
// The below errors cannot occur yet, only once Typst supports full PDF/A // The below errors cannot occur yet, only once Typst supports full PDF/A
// and PDF/UA. // and PDF/UA.
// But let's still add a message just to be on the safe side. // But let's still add a message just to be on the safe side.
ValidationError::MissingAnnotationAltText => { ValidationError::MissingAnnotationAltText => {
bail!(Span::detached(), "{prefix} missing annotation alt text"; error!(Span::detached(), "{prefix} missing annotation alt text";
hint: "please report this as a bug"); hint: "please report this as a bug")
} }
ValidationError::MissingAltText => { ValidationError::MissingAltText => {
bail!(Span::detached(), "{prefix} missing alt text"; error!(Span::detached(), "{prefix} missing alt text";
hint: "make sure your images and formulas have alt text"); hint: "make sure your images and formulas have alt text")
} }
ValidationError::NoDocumentLanguage => { ValidationError::NoDocumentLanguage => {
bail!(Span::detached(), "{prefix} missing document language"; error!(Span::detached(), "{prefix} missing document language";
hint: "set the language of the document"); hint: "set the language of the document")
} }
// Needs to be set by typst-pdf. // Needs to be set by typst-pdf.
ValidationError::MissingHeadingTitle => { ValidationError::MissingHeadingTitle => {
bail!(Span::detached(), "{prefix} missing heading title"; error!(Span::detached(), "{prefix} missing heading title";
hint: "please report this as a bug"); hint: "please report this as a bug")
} }
ValidationError::MissingDocumentOutline => { ValidationError::MissingDocumentOutline => {
bail!(Span::detached(), "{prefix} missing document outline"; error!(Span::detached(), "{prefix} missing document outline";
hint: "please report this as a bug"); hint: "please report this as a bug")
} }
ValidationError::MissingTagging => { ValidationError::MissingTagging => {
bail!(Span::detached(), "{prefix} missing document tags"; error!(Span::detached(), "{prefix} missing document tags";
hint: "please report this as a bug"); hint: "please report this as a bug")
} }
ValidationError::NoDocumentTitle => { ValidationError::NoDocumentTitle => {
bail!(Span::detached(), "{prefix} missing document title"; error!(Span::detached(), "{prefix} missing document title";
hint: "set the title of the document"); hint: "set the title of the document")
} }
} }
})
.collect::<Vec<_>>();
// Deduplicate errors with unspanned tags.
errors.dedup();
Err(errors.into_iter().collect::<EcoVec<_>>())
} }
KrillaError::ImageError(i) => { KrillaError::ImageError(i) => {
let span = gc.image_spans.get(&i).unwrap(); let span = gc.image_to_spans.get(&i).unwrap();
bail!(*span, "failed to process image"); bail!(*span, "failed to process image");
} }
}, },

View File

@ -25,6 +25,7 @@ pub(crate) fn handle_image(
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
surface.push_transform(&fc.state().transform().to_krilla()); surface.push_transform(&fc.state().transform().to_krilla());
surface.set_location(span.into_raw().get());
let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth); let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth);
@ -35,8 +36,9 @@ pub(crate) fn handle_image(
Some(i) => i, Some(i) => i,
}; };
if gc.image_spans.contains_key(&image) { if !gc.image_to_spans.contains_key(&image) {
gc.image_spans.insert(image.clone(), span); gc.image_to_spans.insert(image.clone(), span);
gc.image_spans.insert(span);
} }
surface.draw_image(image, size.to_krilla()); surface.draw_image(image, size.to_krilla());
@ -51,6 +53,7 @@ pub(crate) fn handle_image(
} }
surface.pop(); surface.pop();
surface.reset_location();
Ok(()) Ok(())
} }

View File

@ -1,19 +1,21 @@
use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{convert_path, AbsExt, TransformExt};
use krilla::geom::Rect; use krilla::geom::Rect;
use krilla::path::{Path, PathBuilder}; use krilla::path::{Path, PathBuilder};
use krilla::surface::Surface; use krilla::surface::Surface;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::visualize::{Geometry, Shape}; use typst_library::visualize::{Geometry, Shape};
use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{convert_path, AbsExt, TransformExt};
pub(crate) fn handle_shape( pub(crate) fn handle_shape(
fc: &mut FrameContext, fc: &mut FrameContext,
shape: &Shape, shape: &Shape,
surface: &mut Surface, surface: &mut Surface,
gc: &mut GlobalContext, gc: &mut GlobalContext,
span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
surface.set_location(span.into_raw().get());
surface.push_transform(&fc.state().transform().to_krilla()); surface.push_transform(&fc.state().transform().to_krilla());
if let Some(path) = convert_geometry(&shape.geometry) { if let Some(path) = convert_geometry(&shape.geometry) {
@ -54,6 +56,7 @@ pub(crate) fn handle_shape(
} }
surface.pop(); surface.pop();
surface.reset_location();
Ok(()) Ok(())
} }

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use krilla::font::{GlyphId, GlyphUnits}; use krilla::font::{GlyphId, GlyphUnits};
use krilla::surface::Surface; use krilla::surface::{Location, Surface};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::layout::Size; use typst_library::layout::Size;
use typst_library::text::{Font, Glyph, TextItem}; use typst_library::text::{Font, Glyph, TextItem};
@ -125,4 +125,8 @@ impl krilla::font::Glyph for PdfGlyph {
fn y_advance(&self) -> f32 { fn y_advance(&self) -> f32 {
0.0 0.0
} }
fn location(&self) -> Option<Location> {
Some(self.0.span.0.into_raw().get())
}
} }

View File

@ -113,8 +113,7 @@ impl ColorExt for Color {
/// Display the font family and variant of a font. /// Display the font family and variant of a font.
pub(crate) fn display_font(font: &Font) -> String { pub(crate) fn display_font(font: &Font) -> String {
let font_family = &font.info().family; let font_family = &font.info().family;
let font_variant = font.info().variant; format!("{font_family}")
format!("{font_family} ({font_variant:?})")
} }
/// Build a typst path using a path builder. /// Build a typst path using a path builder.