mirror of
https://github.com/typst/typst
synced 2025-08-15 07:28:32 +08:00
First version of better error support
This commit is contained in:
parent
84dcaaeae2
commit
ccec0dedbf
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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>| {
|
||||||
ValidationError::TooLongString => {
|
loc.map(|l| Span::from_raw(NonZeroU64::new(l).unwrap()))
|
||||||
bail!(Span::detached(), "{prefix} a PDF string longer \
|
.unwrap_or(Span::detached())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut errors = ve.iter().map(|e| {
|
||||||
|
match e {
|
||||||
|
ValidationError::TooLongString => {
|
||||||
|
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());
|
||||||
}
|
|
||||||
ValidationError::InvalidCodepointMapping(_, _) => {
|
error!(span, "{prefix} the text '{text}' cannot be displayed using {font_str}";
|
||||||
bail!(Span::detached(), "{prefix} the PDF contains \
|
hint: "try using a different font"
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
ValidationError::InvalidCodepointMapping(_, _, _, loc) => {
|
||||||
|
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.
|
||||||
|
ValidationError::MissingHeadingTitle => {
|
||||||
|
error!(Span::detached(), "{prefix} missing heading title";
|
||||||
|
hint: "please report this as a bug")
|
||||||
|
}
|
||||||
|
ValidationError::MissingDocumentOutline => {
|
||||||
|
error!(Span::detached(), "{prefix} missing document outline";
|
||||||
|
hint: "please report this as a bug")
|
||||||
|
}
|
||||||
|
ValidationError::MissingTagging => {
|
||||||
|
error!(Span::detached(), "{prefix} missing document tags";
|
||||||
|
hint: "please report this as a bug")
|
||||||
|
}
|
||||||
|
ValidationError::NoDocumentTitle => {
|
||||||
|
error!(Span::detached(), "{prefix} missing document title";
|
||||||
|
hint: "set the title of the document")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Needs to be set by typst-pdf.
|
// Deduplicate errors with unspanned tags.
|
||||||
ValidationError::MissingHeadingTitle => {
|
errors.dedup();
|
||||||
bail!(Span::detached(), "{prefix} missing heading title";
|
|
||||||
hint: "please report this as a bug");
|
Err(errors.into_iter().collect::<EcoVec<_>>())
|
||||||
}
|
|
||||||
ValidationError::MissingDocumentOutline => {
|
|
||||||
bail!(Span::detached(), "{prefix} missing document outline";
|
|
||||||
hint: "please report this as a bug");
|
|
||||||
}
|
|
||||||
ValidationError::MissingTagging => {
|
|
||||||
bail!(Span::detached(), "{prefix} missing document tags";
|
|
||||||
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) => {
|
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");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user