Compare commits

...

13 Commits

Author SHA1 Message Date
Laurenz Stampfl
e4cdaadcfb
Merge 77c25aed746e106b034406435c702d4a875952af into b790c6d59ceaf7a809cc24b60c1f1509807470e2 2025-07-20 13:23:42 +00:00
Laurenz Stampfl
77c25aed74 Update dependencies 2025-07-20 15:23:34 +02:00
Laurenz Stampfl
e71057536b Rename test 2025-07-20 15:22:08 +02:00
Laurenz Stampfl
2f72a1f197 Add tests 2025-07-20 15:13:34 +02:00
Laurenz Stampfl
62eab1dc9f Remove outdated comment 2025-07-20 14:50:02 +02:00
Laurenz Stampfl
7791162455 Group imports 2025-07-20 14:49:34 +02:00
Laurenz Stampfl
24d68ba61c Update documentation 2025-07-20 14:48:50 +02:00
Laurenz Stampfl
34d26dc64b
Merge branch 'main' into pdf_embed 2025-07-20 14:41:00 +02:00
Laurenz Stampfl
62daf7d3ef Add proper errors for PDFs that failed to load 2025-07-20 14:40:14 +02:00
Laurenz Stampfl
d9fb17edb3 Embed all fonts 2025-07-20 14:30:08 +02:00
Erik
b790c6d59c
Add rust-analyzer to flake devShell (#6618) 2025-07-18 14:36:10 +00:00
Malo
b1c79b50d4
Fix documentation oneliners (#6608) 2025-07-18 13:25:17 +00:00
Patrick Massot
4629ede020
Mention Tinymist in README.md (#6601) 2025-07-18 13:21:36 +00:00
15 changed files with 157 additions and 121 deletions

17
Cargo.lock generated
View File

@ -967,7 +967,7 @@ dependencies = [
[[package]]
name = "hayro"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e"
source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d"
dependencies = [
"bytemuck",
"hayro-interpret",
@ -980,7 +980,7 @@ dependencies = [
[[package]]
name = "hayro-font"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e"
source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d"
dependencies = [
"log",
"phf",
@ -989,7 +989,7 @@ dependencies = [
[[package]]
name = "hayro-interpret"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e"
source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d"
dependencies = [
"bitflags 2.9.1",
"hayro-font",
@ -1006,7 +1006,7 @@ dependencies = [
[[package]]
name = "hayro-syntax"
version = "0.0.1"
source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e"
source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d"
dependencies = [
"flate2",
"kurbo",
@ -1019,7 +1019,7 @@ dependencies = [
[[package]]
name = "hayro-write"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=2b63dc8#2b63dc85b9447a815cc5b40c16338841c3780f7e"
source = "git+https://github.com/LaurenzV/hayro?rev=d651e18#d651e188a747c188db2e206733eaf17e8f622a5d"
dependencies = [
"flate2",
"hayro-syntax",
@ -1430,7 +1430,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.4.0"
source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d"
source = "git+https://github.com/LaurenzV/krilla/?rev=2da9d6c#2da9d6c6cb6a6baa6379592c72ae4eb7e16aa7b4"
dependencies = [
"base64",
"bumpalo",
@ -1460,7 +1460,7 @@ dependencies = [
[[package]]
name = "krilla-svg"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/krilla?rev=1668ac2#1668ac2e64dc85572e6c62a2399e85acd39b619d"
source = "git+https://github.com/LaurenzV/krilla/?rev=2da9d6c#2da9d6c6cb6a6baa6379592c72ae4eb7e16aa7b4"
dependencies = [
"flate2",
"fontdb",
@ -2926,6 +2926,7 @@ dependencies = [
[[package]]
name = "typst-assets"
version = "0.13.1"
source = "git+https://github.com/LaurenzV/typst-assets?rev=d89cc82#d89cc821f84a5667714491019c0b64087b2608bd"
[[package]]
name = "typst-cli"
@ -2975,7 +2976,7 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
source = "git+https://github.com/LaurenzV/typst-dev-assets?rev=180c145#180c145cf810c8b2a7ed77355b351f3aed07d0c6"
[[package]]
name = "typst-docs"

View File

@ -32,8 +32,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { path = "../typst-assets" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
typst-assets = { git = "https://github.com/LaurenzV/typst-assets", rev = "d89cc82" }
typst-dev-assets = { git = "https://github.com/LaurenzV/typst-dev-assets", rev = "180c145" }
arrayvec = "0.7.4"
az = "1.2"
base64 = "0.22"
@ -61,8 +61,8 @@ fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3"
glidesort = "0.1.2"
hayagriva = "0.8.1"
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" }
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "2b63dc8" }
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "d651e18" }
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "d651e18" }
heck = "0.5"
hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] }
@ -75,8 +75,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg
indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "1668ac2" }
krilla = { git = "https://github.com/LaurenzV/krilla/", rev = "2da9d6c", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla/", rev = "2da9d6c"}
kurbo = "0.11"
libfuzzer-sys = "0.4"
lipsum = "0.9"

View File

@ -173,8 +173,11 @@ typst help
typst help watch
```
If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out [Typst's free web app][app].
If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out our [free web app][app]. Alternatively, there is
a community-created language server called
[Tinymist](https://myriad-dreamin.github.io/tinymist/) which is integrated into
various editor extensions.
## Community
The main places where the community gathers are our [Forum][forum] and our

View File

@ -797,7 +797,9 @@ impl Color {
components
}
/// Returns the constructor function for this color's space:
/// Returns the constructor function for this color's space.
///
/// Returns one of:
/// - [`luma`]($color.luma)
/// - [`oklab`]($color.oklab)
/// - [`oklch`]($color.oklch)

View File

@ -15,6 +15,7 @@ use std::fmt::{self, Debug, Formatter};
use std::sync::Arc;
use ecow::EcoString;
use hayro_syntax::LoadPdfError;
use typst_library::{Feature, World};
use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash;
@ -272,9 +273,27 @@ impl Packed<ImageElem> {
),
ImageFormat::Vector(VectorFormat::Pdf) => {
if engine.world.library().features.is_enabled(Feature::PdfEmbedding) {
let document =
PdfDocument::new(loaded.data.clone(), engine.world.clone())
.within(loaded)?;
let document = match PdfDocument::new(loaded.data.clone()) {
Ok(doc) => doc,
Err(e) => match e {
LoadPdfError::Encryption => {
bail!(
span,
"the PDF is encrypted or password-protected";
hint: "such PDFs are currently not supported";
hint: "preprocess the PDF to remove the encryption"
);
}
LoadPdfError::Invalid => {
bail!(
span,
"the PDF could not be loaded";
hint: "perhaps the PDF file is malformed"
);
}
},
};
let page_num = self.page.get(styles);
if page_num == 0 {
@ -285,16 +304,18 @@ impl Packed<ImageElem> {
)
};
// The user provides the page number start from 1, further down the pipeline,
// The user provides the page number start from 1, but further down the pipeline,
// page numbers are 0-based.
let page_idx = page_num - 1;
let num_pages = document.len();
let Some(pdf_image) = PdfImage::new(document, page_idx) else {
let pages = if num_pages == 1 { "page" } else { "pages" };
bail!(
span,
"page {page_num} doesn't exist";
hint: "the document only has {num_pages} pages"
hint: "the document only has {num_pages} {pages}"
);
};

View File

@ -1,13 +1,10 @@
use crate::diag::LoadResult;
use crate::foundations::Bytes;
use crate::text::{FontStretch, FontStyle, FontVariant, FontWeight};
use crate::World;
use comemo::Tracked;
use hayro_syntax::page::Page;
use hayro_syntax::Pdf;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use typst_library::text::FontInfo;
use hayro_syntax::page::Page;
use hayro_syntax::{LoadPdfError, Pdf};
use crate::foundations::Bytes;
struct DocumentRepr {
pdf: Arc<Pdf>,
@ -29,24 +26,24 @@ impl PdfDocument {
/// Load a PDF document.
#[comemo::memoize]
#[typst_macros::time(name = "load pdf document")]
pub fn new(data: Bytes, world: Tracked<dyn World + '_>) -> LoadResult<PdfDocument> {
// TODO: Remove unwraps
let pdf = Arc::new(Pdf::new(Arc::new(data.clone())).unwrap());
let standard_fonts = get_standard_fonts(world.clone());
pub fn new(data: Bytes) -> Result<PdfDocument, LoadPdfError> {
let pdf = Arc::new(Pdf::new(Arc::new(data.clone()))?);
let standard_fonts = get_standard_fonts();
Ok(Self(Arc::new(DocumentRepr { data, pdf, standard_fonts })))
}
/// Return the number of pages in the PDF.
pub fn len(&self) -> usize {
self.0.pdf.pages().len()
}
}
struct ImageRepr {
pub document: PdfDocument,
pub page_index: usize,
pub width: f32,
pub height: f32,
document: PdfDocument,
page_index: usize,
width: f32,
height: f32,
}
impl Hash for ImageRepr {
@ -56,16 +53,16 @@ impl Hash for ImageRepr {
}
}
/// A page of a PDF file.
/// A specific page of a PDF acting as an image.
#[derive(Clone, Hash)]
pub struct PdfImage(Arc<ImageRepr>);
impl PdfImage {
/// Create a new PDF image. Returns `None` if the page index is not valid.
/// Create a new PDF image.
///
/// Returns `None` if the page index is not valid.
#[comemo::memoize]
pub fn new(document: PdfDocument, page: usize) -> Option<PdfImage> {
// TODO: Don't allow loading if pdf-embedding feature is disabled.
// TODO: Remove Unwrap
let dimensions = document.0.pdf.pages().get(page)?.render_dimensions();
Some(Self(Arc::new(ImageRepr {
@ -76,110 +73,93 @@ impl PdfImage {
})))
}
/// Returns the PDF page of the image.
pub fn page(&self) -> &Page {
&self.0.document.0.pdf.pages()[self.0.page_index]
&self.pdf().pages()[self.0.page_index]
}
/// Returns the underlying PDF document.
pub fn pdf(&self) -> &Arc<Pdf> {
&self.0.document.0.pdf
}
/// Returns the width of the image.
pub fn width(&self) -> f32 {
self.0.width
}
/// Returns the embedded standard fonts of the image.
pub fn standard_fonts(&self) -> &Arc<StandardFonts> {
&self.0.document.0.standard_fonts
}
/// Returns the height of the image.
pub fn height(&self) -> f32 {
self.0.height
}
pub fn data(&self) -> &Bytes {
&self.0.document.0.data
}
/// Returns the page index of the image.
pub fn page_index(&self) -> usize {
self.0.page_index
}
/// Returns the underlying Typst PDF document.
pub fn document(&self) -> &PdfDocument {
&self.0.document
}
}
#[comemo::memoize]
fn get_standard_fonts(world: Tracked<dyn World + '_>) -> Arc<StandardFonts> {
let book = world.book();
let get_font = |name: &str, fallback_name: Option<&str>, variant: FontVariant| {
book.select(name, variant)
.or_else(|| {
if let Some(fallback_name) = fallback_name {
book.select(fallback_name, variant)
} else {
None
}
})
.or_else(|| book.select_fallback(None, variant, "A"))
.and_then(|i| world.font(i))
.map(|font| (font.data().clone(), font.index()))
};
let normal_variant = FontVariant::new(
FontStyle::Normal,
FontWeight::default(),
FontStretch::default(),
);
let bold_variant =
FontVariant::new(FontStyle::Normal, FontWeight::BOLD, FontStretch::default());
let italic_variant = FontVariant::new(
FontStyle::Italic,
FontWeight::default(),
FontStretch::default(),
);
let bold_italic_variant =
FontVariant::new(FontStyle::Italic, FontWeight::BOLD, FontStretch::default());
fn get_standard_fonts() -> Arc<StandardFonts> {
let helvetica = VariantFont {
normal: get_font("helvetica", Some("liberation sans"), normal_variant),
bold: get_font("helvetica", Some("liberation sans"), bold_variant),
italic: get_font("helvetica", Some("liberation sans"), italic_variant),
bold_italic: get_font("helvetica", Some("liberation sans"), bold_italic_variant),
normal: Bytes::new(typst_assets::pdf::SANS),
bold: Bytes::new(typst_assets::pdf::SANS_BOLD),
italic: Bytes::new(typst_assets::pdf::SANS_ITALIC),
bold_italic: Bytes::new(typst_assets::pdf::SANS_BOLD_ITALIC),
};
let courier = VariantFont {
normal: get_font("courier", Some("liberation mono"), normal_variant),
bold: get_font("courier", Some("liberation mono"), bold_variant),
italic: get_font("courier", Some("liberation mono"), italic_variant),
bold_italic: get_font("courier", Some("liberation mono"), bold_italic_variant),
normal: Bytes::new(typst_assets::pdf::FIXED),
bold: Bytes::new(typst_assets::pdf::FIXED_BOLD),
italic: Bytes::new(typst_assets::pdf::FIXED_ITALIC),
bold_italic: Bytes::new(typst_assets::pdf::FIXED_BOLD_ITALIC),
};
let times = VariantFont {
normal: get_font("times", Some("liberation serif"), normal_variant),
bold: get_font("times", Some("liberation serif"), bold_variant),
italic: get_font("times", Some("liberation serif"), italic_variant),
bold_italic: get_font("times", Some("liberation serif"), bold_italic_variant),
normal: Bytes::new(typst_assets::pdf::SERIF),
bold: Bytes::new(typst_assets::pdf::SERIF_BOLD),
italic: Bytes::new(typst_assets::pdf::SERIF_ITALIC),
bold_italic: Bytes::new(typst_assets::pdf::SERIF_BOLD_ITALIC),
};
let symbol = Some(Bytes::new(typst_assets::pdf::SYMBOL));
let zapf_dingbats = Some(Bytes::new(typst_assets::pdf::DING_BATS));
let symbol = Bytes::new(typst_assets::pdf::SYMBOL);
let zapf_dingbats = Bytes::new(typst_assets::pdf::DING_BATS);
Arc::new(StandardFonts { helvetica, courier, times, symbol, zapf_dingbats })
}
/// A PDF font with multiple variants.
pub struct VariantFont {
pub normal: Option<(Bytes, u32)>,
pub bold: Option<(Bytes, u32)>,
pub italic: Option<(Bytes, u32)>,
pub bold_italic: Option<(Bytes, u32)>,
/// The normal variant.
pub normal: Bytes,
/// The bold variant.
pub bold: Bytes,
/// The italic variant.
pub italic: Bytes,
/// The bold-italic variant.
pub bold_italic: Bytes,
}
/// A structure holding the raw data of all PDF standard fonts.
pub struct StandardFonts {
/// The data for the `Helvetica` font family.
pub helvetica: VariantFont,
/// The data for the `Courier` font family.
pub courier: VariantFont,
/// The data for the `Times` font family.
pub times: VariantFont,
pub symbol: Option<Bytes>,
pub zapf_dingbats: Option<Bytes>,
/// The data for the `Symbol` font family.
pub symbol: Bytes,
/// The data for the `Zapf Dingbats` font family.
pub zapf_dingbats: Bytes,
}

View File

@ -124,15 +124,13 @@ fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option<sk::Pixmap> {
StandardFont::TimesBold => sf.times.bold.clone(),
StandardFont::TimesItalic => sf.times.italic.clone(),
StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(),
StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)),
StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)),
StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(),
StandardFont::Symbol => sf.symbol.clone(),
};
bytes.map(|d| {
let font_data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(d.0.clone());
let font_data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(bytes.clone());
(font_data, d.1)
})
Some((font_data, 0))
};
let interpreter_settings = InterpreterSettings {

View File

@ -116,15 +116,13 @@ fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec<u8> {
StandardFont::TimesBold => sf.times.bold.clone(),
StandardFont::TimesItalic => sf.times.italic.clone(),
StandardFont::TimesBoldItalic => sf.times.bold_italic.clone(),
StandardFont::ZapfDingBats => sf.zapf_dingbats.clone().map(|d| (d, 0)),
StandardFont::Symbol => sf.symbol.clone().map(|d| (d, 0)),
StandardFont::ZapfDingBats => sf.zapf_dingbats.clone(),
StandardFont::Symbol => sf.symbol.clone(),
};
bytes.map(|d| {
let font_data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(d.0.clone());
let font_data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(bytes.clone());
(font_data, d.1)
})
Some((font_data, 0))
};
let interpreter_settings = InterpreterSettings {

View File

@ -242,7 +242,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem {
name: group.name.clone(),
route: subpage.route.clone(),
oneliner: oneliner(docs).into(),
oneliner: oneliner(docs),
code: true,
});
children.push(subpage);
@ -296,7 +296,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem {
name: name.into(),
route: subpage.route.clone(),
oneliner: oneliner(func.docs().unwrap_or_default()).into(),
oneliner: oneliner(func.docs().unwrap_or_default()),
code: true,
});
children.push(subpage);
@ -306,7 +306,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
items.push(CategoryItem {
name: ty.short_name().into(),
route: subpage.route.clone(),
oneliner: oneliner(ty.docs()).into(),
oneliner: oneliner(ty.docs()),
code: true,
});
children.push(subpage);
@ -637,7 +637,7 @@ fn group_page(
let item = CategoryItem {
name: group.name.clone(),
route: model.route.clone(),
oneliner: oneliner(&group.details).into(),
oneliner: oneliner(&group.details),
code: false,
};
@ -772,8 +772,24 @@ pub fn urlify(title: &str) -> EcoString {
}
/// Extract the first line of documentation.
fn oneliner(docs: &str) -> &str {
docs.lines().next().unwrap_or_default()
fn oneliner(docs: &str) -> EcoString {
let paragraph = docs.split("\n\n").next().unwrap_or_default();
let mut depth = 0;
let mut period = false;
let mut end = paragraph.len();
for (i, c) in paragraph.char_indices() {
match c {
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
'.' if depth == 0 => period = true,
c if period && c.is_whitespace() && !docs[..i].ends_with("e.g.") => {
end = i;
break;
}
_ => period = false,
}
}
EcoString::from(&docs[..end]).replace("\r\n", " ").replace("\n", " ")
}
/// The order of types in the documentation.

View File

@ -86,7 +86,7 @@ pub struct FuncModel {
pub name: EcoString,
pub title: &'static str,
pub keywords: &'static [&'static str],
pub oneliner: &'static str,
pub oneliner: EcoString,
pub element: bool,
pub contextual: bool,
pub deprecation: Option<&'static str>,
@ -139,7 +139,7 @@ pub struct TypeModel {
pub name: &'static str,
pub title: &'static str,
pub keywords: &'static [&'static str],
pub oneliner: &'static str,
pub oneliner: EcoString,
pub details: Html,
pub constructor: Option<FuncModel>,
pub scope: Vec<FuncModel>,

View File

@ -127,6 +127,10 @@
checks = self'.checks;
inputsFrom = [ typst ];
buildInputs = with pkgs; [
rust-analyzer
];
packages = [
# A script for quickly running tests.
# See https://github.com/typst/typst/blob/main/tests/README.md#making-an-alias

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
tests/ref/image-pdf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -198,7 +198,7 @@ fn library() -> Library {
// exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers.
let mut lib = Library::builder()
.with_features([Feature::Html].into_iter().collect())
.with_features([Feature::Html, Feature::PdfEmbedding].into_iter().collect())
.build();
// Hook up helpers into the global scope.

View File

@ -258,7 +258,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
--- image-png-but-pixmap-format ---
#image(
read("/assets/images/tiger.jpg", encoding: none),
// Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", or auto
// Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", "pdf", or auto
format: "rgba8",
)
@ -289,3 +289,16 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
..rotations.map(v => raw(str(v), lang: "typc")),
..rotations.map(rotated)
)
--- image-pdf ---
#image("/assets/images/matplotlib.pdf")
--- image-pdf-invalid-page ---
// Error: 2-49 page 2 doesn't exist
// Hint: 2-49 the document only has 1 page
#image("/assets/images/matplotlib.pdf", page: 2)
--- image-pdf-multiple-pages ---
#image("/assets/images/diagrams.pdf", page: 1)
#image("/assets/images/diagrams.pdf", page: 3)
#image("/assets/images/diagrams.pdf", page: 2)