Compare commits

...

6 Commits

Author SHA1 Message Date
Andrew Voynov
a2e87185c1
Merge 6daae2e292ad148a458c04d9a244021373063649 into af2253ba16dfdc731e787e3a43a6f6a63ea65e0a 2025-07-22 19:21:54 +08:00
Laurenz Stampfl
af2253ba16
Add support for PDF embedding (#6623)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-07-22 11:06:44 +00:00
Andrew Voynov
6daae2e292
Refactor "Making a Template" tutorial 2025-06-12 09:03:09 +03:00
Andrew Voynov
7f24cd9253
Refactor "Writing in Typst" tutorial 2025-06-12 09:01:57 +03:00
Andrew Voynov
fea153a6fc
Refactor "Formatting" tutorial 2025-06-12 09:01:57 +03:00
Andrew Voynov
cdb8a42c68
Refactor "Advanced Styling" tutorial 2025-06-12 09:01:50 +03:00
20 changed files with 867 additions and 411 deletions

124
Cargo.lock generated
View File

@ -181,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.8.0" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -214,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.21.0" version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
@ -964,6 +964,69 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "hayro"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"bytemuck",
"hayro-interpret",
"image",
"kurbo",
"rustc-hash",
"smallvec",
]
[[package]]
name = "hayro-font"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"log",
"phf",
]
[[package]]
name = "hayro-interpret"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"bitflags 2.9.1",
"hayro-font",
"hayro-syntax",
"kurbo",
"log",
"phf",
"qcms",
"skrifa",
"smallvec",
"yoke 0.8.0",
]
[[package]]
name = "hayro-syntax"
version = "0.0.1"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"flate2",
"kurbo",
"log",
"rustc-hash",
"smallvec",
"zune-jpeg",
]
[[package]]
name = "hayro-write"
version = "0.1.0"
source = "git+https://github.com/LaurenzV/hayro?rev=e701f95#e701f9569157a2fe4ade68930dc9e9283782dcca"
dependencies = [
"flate2",
"hayro-syntax",
"log",
"pdf-writer",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@ -1200,9 +1263,9 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.5" version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@ -1265,7 +1328,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"inotify-sys", "inotify-sys",
"libc", "libc",
] ]
@ -1361,7 +1424,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1370,6 +1433,7 @@ dependencies = [
"float-cmp 0.10.0", "float-cmp 0.10.0",
"fxhash", "fxhash",
"gif", "gif",
"hayro-write",
"image-webp", "image-webp",
"imagesize", "imagesize",
"once_cell", "once_cell",
@ -1379,6 +1443,7 @@ dependencies = [
"rustybuzz", "rustybuzz",
"siphasher", "siphasher",
"skrifa", "skrifa",
"smallvec",
"subsetter", "subsetter",
"tiny-skia-path", "tiny-skia-path",
"xmp-writer", "xmp-writer",
@ -1389,7 +1454,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" source = "git+https://github.com/LaurenzV/krilla?rev=37b9a00#37b9a00bfac87ed0b347b7cf8e9d37a6f68fcccd"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",
@ -1402,9 +1467,9 @@ dependencies = [
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" checksum = "1077d333efea6170d9ccb96d3c3026f300ca0773da4938cc4c811daa6df68b0c"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"smallvec", "smallvec",
@ -1456,7 +1521,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"libc", "libc",
"redox_syscall", "redox_syscall",
] ]
@ -1622,7 +1687,7 @@ version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
@ -1704,7 +1769,7 @@ version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"cfg-if", "cfg-if",
"foreign-types", "foreign-types",
"libc", "libc",
@ -1841,7 +1906,7 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc" checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@ -1999,7 +2064,7 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"getopts", "getopts",
"memchr", "memchr",
"unicase", "unicase",
@ -2112,7 +2177,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]
@ -2215,7 +2280,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -2234,7 +2299,7 @@ version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"bytemuck", "bytemuck",
"core_maths", "core_maths",
"log", "log",
@ -2282,7 +2347,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -2445,9 +2510,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.13.2" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "spin" name = "spin"
@ -2855,7 +2920,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" source = "git+https://github.com/typst/typst-assets?rev=fbf00f9#fbf00f9539fdb0825bef4d39fb57d5986c51b756"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -2905,7 +2970,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648" source = "git+https://github.com/typst/typst-dev-assets?rev=c6c2acf#c6c2acf6cdc31f99a23a478d3d614f8bf806a4f5"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"
@ -3055,7 +3120,7 @@ name = "typst-library"
version = "0.13.1" version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bitflags 2.8.0", "bitflags 2.9.1",
"bumpalo", "bumpalo",
"chinese-number", "chinese-number",
"ciborium", "ciborium",
@ -3067,6 +3132,7 @@ dependencies = [
"fontdb", "fontdb",
"glidesort", "glidesort",
"hayagriva", "hayagriva",
"hayro-syntax",
"icu_properties", "icu_properties",
"icu_provider", "icu_provider",
"icu_provider_blob", "icu_provider_blob",
@ -3165,11 +3231,13 @@ version = "0.13.1"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"comemo", "comemo",
"hayro",
"image", "image",
"pixglyph", "pixglyph",
"resvg", "resvg",
"tiny-skia", "tiny-skia",
"ttf-parser", "ttf-parser",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-timing", "typst-timing",
@ -3183,8 +3251,10 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"flate2", "flate2",
"hayro",
"image", "image",
"ttf-parser", "ttf-parser",
"typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-timing", "typst-timing",
@ -3581,7 +3651,7 @@ version = "0.221.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
"indexmap 2.7.1", "indexmap 2.7.1",
] ]
@ -3716,7 +3786,7 @@ version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.9.1",
] ]
[[package]] [[package]]

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-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", 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-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fbf00f9" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "c6c2acf" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -61,6 +61,8 @@ fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
glidesort = "0.1.2" glidesort = "0.1.2"
hayagriva = "0.8.1" hayagriva = "0.8.1"
hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
hayro = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" }
heck = "0.5" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] } icu_properties = { version = "1.4", features = ["serde"] }
@ -72,8 +74,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false } infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" } krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "37b9a00"}
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"

View File

@ -31,6 +31,7 @@ flate2 = { workspace = true }
fontdb = { workspace = true } fontdb = { workspace = true }
glidesort = { workspace = true } glidesort = { workspace = true }
hayagriva = { workspace = true } hayagriva = { workspace = true }
hayro-syntax = { workspace = true }
icu_properties = { workspace = true } icu_properties = { workspace = true }
icu_provider = { workspace = true } icu_provider = { workspace = true }
icu_provider_blob = { workspace = true } icu_provider_blob = { workspace = true }

View File

@ -1,8 +1,10 @@
//! Image handling. //! Image handling.
mod pdf;
mod raster; mod raster;
mod svg; mod svg;
pub use self::pdf::PdfImage;
pub use self::raster::{ pub use self::raster::{
ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage, ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage,
}; };
@ -10,13 +12,15 @@ pub use self::svg::SvgImage;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use ecow::EcoString; use ecow::EcoString;
use hayro_syntax::LoadPdfError;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use typst_utils::LazyHash; use typst_utils::{LazyHash, NonZeroExt};
use crate::diag::{At, LoadedWithin, SourceResult, StrResult, warning}; use crate::diag::{At, LoadedWithin, SourceResult, StrResult, bail, warning};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, StyleChain, cast, elem, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, StyleChain, cast, elem,
@ -26,6 +30,7 @@ use crate::layout::{Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable; use crate::model::Figurable;
use crate::text::{LocalName, families}; use crate::text::{LocalName, families};
use crate::visualize::image::pdf::PdfDocument;
/// A raster or vector graphic. /// A raster or vector graphic.
/// ///
@ -79,8 +84,7 @@ pub struct ImageElem {
/// format automatically, but that's not always possible). /// format automatically, but that's not always possible).
/// ///
/// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`, /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`,
/// `{"webp"}` as well as raw pixel data. Embedding PDFs as images is /// `{"pdf"}`, `{"webp"}` as well as raw pixel data.
/// [not currently supported](https://github.com/typst/typst/issues/145).
/// ///
/// When providing raw pixel data as the `source`, you must specify a /// When providing raw pixel data as the `source`, you must specify a
/// dictionary with the following keys as the `format`: /// dictionary with the following keys as the `format`:
@ -126,6 +130,11 @@ pub struct ImageElem {
/// A text describing the image. /// A text describing the image.
pub alt: Option<EcoString>, pub alt: Option<EcoString>,
/// The page number that should be embedded as an image. This attribute only
/// has an effect for PDF files.
#[default(NonZeroUsize::ONE)]
pub page: NonZeroUsize,
/// How the image should adjust itself to a given area (the area is defined /// How the image should adjust itself to a given area (the area is defined
/// by the `width` and `height` fields). Note that `fit` doesn't visually /// by the `width` and `height` fields). Note that `fit` doesn't visually
/// change anything if the area's aspect ratio is the same as the image's /// change anything if the area's aspect ratio is the same as the image's
@ -261,6 +270,45 @@ impl Packed<ImageElem> {
) )
.within(loaded)?, .within(loaded)?,
), ),
ImageFormat::Vector(VectorFormat::Pdf) => {
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"
);
}
},
};
// The user provides the page number start from 1, but further
// down the pipeline, page numbers are 0-based.
let page_num = self.page.get(styles).get();
let page_idx = page_num - 1;
let num_pages = document.num_pages();
let Some(pdf_image) = PdfImage::new(document, page_idx) else {
let s = if num_pages == 1 { "" } else { "s" };
bail!(
span,
"page {page_num} does not exist";
hint: "the document only has {num_pages} page{s}"
);
};
ImageKind::Pdf(pdf_image)
}
}; };
Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles))) Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles)))
@ -286,6 +334,7 @@ impl Packed<ImageElem> {
"jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => return Ok(ExchangeFormat::Gif.into()), "gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => return Ok(VectorFormat::Svg.into()), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
"pdf" => return Ok(VectorFormat::Pdf.into()),
"webp" => return Ok(ExchangeFormat::Webp.into()), "webp" => return Ok(ExchangeFormat::Webp.into()),
_ => {} _ => {}
} }
@ -373,6 +422,7 @@ impl Image {
match &self.0.kind { match &self.0.kind {
ImageKind::Raster(raster) => raster.format().into(), ImageKind::Raster(raster) => raster.format().into(),
ImageKind::Svg(_) => VectorFormat::Svg.into(), ImageKind::Svg(_) => VectorFormat::Svg.into(),
ImageKind::Pdf(_) => VectorFormat::Pdf.into(),
} }
} }
@ -381,6 +431,7 @@ impl Image {
match &self.0.kind { match &self.0.kind {
ImageKind::Raster(raster) => raster.width() as f64, ImageKind::Raster(raster) => raster.width() as f64,
ImageKind::Svg(svg) => svg.width(), ImageKind::Svg(svg) => svg.width(),
ImageKind::Pdf(pdf) => pdf.width() as f64,
} }
} }
@ -389,6 +440,7 @@ impl Image {
match &self.0.kind { match &self.0.kind {
ImageKind::Raster(raster) => raster.height() as f64, ImageKind::Raster(raster) => raster.height() as f64,
ImageKind::Svg(svg) => svg.height(), ImageKind::Svg(svg) => svg.height(),
ImageKind::Pdf(pdf) => pdf.height() as f64,
} }
} }
@ -397,6 +449,7 @@ impl Image {
match &self.0.kind { match &self.0.kind {
ImageKind::Raster(raster) => raster.dpi(), ImageKind::Raster(raster) => raster.dpi(),
ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI), ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI),
ImageKind::Pdf(_) => Some(Image::DEFAULT_DPI),
} }
} }
@ -435,6 +488,8 @@ pub enum ImageKind {
Raster(RasterImage), Raster(RasterImage),
/// An SVG image. /// An SVG image.
Svg(SvgImage), Svg(SvgImage),
/// A PDF image.
Pdf(PdfImage),
} }
impl From<RasterImage> for ImageKind { impl From<RasterImage> for ImageKind {
@ -469,10 +524,20 @@ impl ImageFormat {
return Some(Self::Vector(VectorFormat::Svg)); return Some(Self::Vector(VectorFormat::Svg));
} }
if is_pdf(data) {
return Some(Self::Vector(VectorFormat::Pdf));
}
None None
} }
} }
/// Checks whether the data looks like a PDF file.
fn is_pdf(data: &[u8]) -> bool {
let head = &data[..data.len().min(2048)];
memchr::memmem::find(head, b"%PDF-").is_some()
}
/// Checks whether the data looks like an SVG or a compressed SVG. /// Checks whether the data looks like an SVG or a compressed SVG.
fn is_svg(data: &[u8]) -> bool { fn is_svg(data: &[u8]) -> bool {
// Check for the gzip magic bytes. This check is perhaps a bit too // Check for the gzip magic bytes. This check is perhaps a bit too
@ -493,6 +558,9 @@ fn is_svg(data: &[u8]) -> bool {
pub enum VectorFormat { pub enum VectorFormat {
/// The vector graphics format of the web. /// The vector graphics format of the web.
Svg, Svg,
/// High-fidelity document and graphics format, with focus on exact
/// reproduction in print.
Pdf,
} }
impl<R> From<R> for ImageFormat impl<R> From<R> for ImageFormat

View File

@ -0,0 +1,98 @@
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use hayro_syntax::page::Page;
use hayro_syntax::{LoadPdfError, Pdf};
use crate::foundations::Bytes;
/// A PDF document.
#[derive(Clone, Hash)]
pub struct PdfDocument(Arc<DocumentRepr>);
/// The internal representation of a `PdfDocument`.
struct DocumentRepr {
pdf: Arc<Pdf>,
data: Bytes,
}
impl PdfDocument {
/// Loads a PDF document.
#[comemo::memoize]
#[typst_macros::time(name = "load pdf document")]
pub fn new(data: Bytes) -> Result<PdfDocument, LoadPdfError> {
let pdf = Arc::new(Pdf::new(Arc::new(data.clone()))?);
Ok(Self(Arc::new(DocumentRepr { data, pdf })))
}
/// Returns the underlying PDF document.
pub fn pdf(&self) -> &Arc<Pdf> {
&self.0.pdf
}
/// Return the number of pages in the PDF.
pub fn num_pages(&self) -> usize {
self.0.pdf.pages().len()
}
}
impl Hash for DocumentRepr {
fn hash<H: Hasher>(&self, state: &mut H) {
self.data.hash(state);
}
}
/// A specific page of a PDF acting as an image.
#[derive(Clone, Hash)]
pub struct PdfImage(Arc<ImageRepr>);
/// The internal representation of a `PdfImage`.
struct ImageRepr {
document: PdfDocument,
page_index: usize,
width: f32,
height: f32,
}
impl PdfImage {
/// Creates a new PDF image.
///
/// Returns `None` if the page index is not valid.
#[comemo::memoize]
pub fn new(document: PdfDocument, page_index: usize) -> Option<PdfImage> {
let (width, height) = document.0.pdf.pages().get(page_index)?.render_dimensions();
Some(Self(Arc::new(ImageRepr { document, page_index, width, height })))
}
/// Returns the underlying Typst PDF document.
pub fn document(&self) -> &PdfDocument {
&self.0.document
}
/// Returns the PDF page of the image.
pub fn page(&self) -> &Page {
&self.document().pdf().pages()[self.0.page_index]
}
/// Returns the width of the image.
pub fn width(&self) -> f32 {
self.0.width
}
/// Returns the height of the image.
pub fn height(&self) -> f32 {
self.0.height
}
/// Returns the page index of the image.
pub fn page_index(&self) -> usize {
self.0.page_index
}
}
impl Hash for ImageRepr {
fn hash<H: Hasher>(&self, state: &mut H) {
self.document.hash(state);
self.page_index.hash(state);
}
}

View File

@ -9,6 +9,7 @@ use krilla::embed::EmbedError;
use krilla::error::KrillaError; use krilla::error::KrillaError;
use krilla::geom::PathBuilder; use krilla::geom::PathBuilder;
use krilla::page::{PageLabel, PageSettings}; use krilla::page::{PageLabel, PageSettings};
use krilla::pdf::PdfError;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::{Document, SerializeSettings}; use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph; use krilla_svg::render_svg_glyph;
@ -363,6 +364,42 @@ fn finish(
hint: "convert the image to 8 bit instead" hint: "convert the image to 8 bit instead"
) )
} }
KrillaError::Pdf(_, e, loc) => {
let span = to_span(loc);
match e {
// We already validated in `typst-library` that the page index is valid.
PdfError::InvalidPage(_) => bail!(
span,
"invalid page number for PDF file";
hint: "please report this as a bug"
),
PdfError::VersionMismatch(v) => {
let pdf_ver = v.as_str();
let config_ver = configuration.version();
let cur_ver = config_ver.as_str();
bail!(span,
"the version of the PDF is too high";
hint: "the current export target is {cur_ver}, while the PDF has version {pdf_ver}";
hint: "raise the export target to {pdf_ver} or higher";
hint: "or preprocess the PDF to convert it to a lower version"
);
}
}
}
KrillaError::DuplicateTagId(_, loc) => {
let span = to_span(loc);
bail!(span,
"duplicate tag id";
hint: "please report this as a bug"
);
}
KrillaError::UnknownTagId(_, loc) => {
let span = to_span(loc);
bail!(span,
"unknown tag id";
hint: "please report this as a bug"
);
}
}, },
} }
} }
@ -535,12 +572,12 @@ fn convert_error(
} }
// 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. But let's still add a message just to be on the safe side. // and PDF/UA. But let's still add a message just to be on the safe side.
ValidationError::MissingAnnotationAltText => error!( ValidationError::MissingAnnotationAltText(_) => error!(
Span::detached(), Span::detached(),
"{prefix} missing annotation alt text"; "{prefix} missing annotation alt text";
hint: "please report this as a bug" hint: "please report this as a bug"
), ),
ValidationError::MissingAltText => error!( ValidationError::MissingAltText(_) => error!(
Span::detached(), Span::detached(),
"{prefix} missing alt text"; "{prefix} missing alt text";
hint: "make sure your images and equations have alt text" hint: "make sure your images and equations have alt text"
@ -576,6 +613,13 @@ fn convert_error(
"{prefix} missing document date"; "{prefix} missing document date";
hint: "set the date of the document" hint: "set the date of the document"
), ),
ValidationError::EmbeddedPDF(loc) => {
error!(
to_span(*loc),
"embedding PDFs is currently not supported in this export mode";
hint: "try converting the PDF to an SVG before embedding it"
)
}
} }
} }

View File

@ -3,13 +3,14 @@ use std::sync::{Arc, OnceLock};
use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba}; use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::pdf::PdfDocument;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla_svg::{SurfaceExt, SvgSettings}; use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{SourceResult, bail}; use typst_library::diag::{SourceResult, bail};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Angle, Ratio, Size, Transform}; use typst_library::layout::{Abs, Angle, Ratio, Size, Transform};
use typst_library::visualize::{ use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, RasterImage, ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat, RasterImage,
}; };
use typst_syntax::Span; use typst_syntax::Span;
@ -60,6 +61,9 @@ pub(crate) fn handle_image(
SvgSettings { embed_text: true, ..Default::default() }, SvgSettings { embed_text: true, ..Default::default() },
); );
} }
ImageKind::Pdf(pdf) => {
surface.draw_pdf_page(&convert_pdf(pdf), size.to_krilla(), pdf.page_index())
}
} }
if image.alt().is_some() { if image.alt().is_some() {
@ -85,9 +89,9 @@ struct Repr {
/// A wrapper around `RasterImage` so that we can implement `CustomImage`. /// A wrapper around `RasterImage` so that we can implement `CustomImage`.
#[derive(Clone)] #[derive(Clone)]
struct PdfImage(Arc<Repr>); struct PdfRasterImage(Arc<Repr>);
impl PdfImage { impl PdfRasterImage {
pub fn new(raster: RasterImage) -> Self { pub fn new(raster: RasterImage) -> Self {
Self(Arc::new(Repr { Self(Arc::new(Repr {
raster, raster,
@ -97,7 +101,7 @@ impl PdfImage {
} }
} }
impl Hash for PdfImage { impl Hash for PdfRasterImage {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
// `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`, // `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`,
// so this is enough. Since `raster` is prehashed, this is also very cheap. // so this is enough. Since `raster` is prehashed, this is also very cheap.
@ -105,7 +109,7 @@ impl Hash for PdfImage {
} }
} }
impl CustomImage for PdfImage { impl CustomImage for PdfRasterImage {
fn color_channel(&self) -> &[u8] { fn color_channel(&self) -> &[u8] {
self.0 self.0
.actual_dynamic .actual_dynamic
@ -196,10 +200,15 @@ fn convert_raster(
interpolate, interpolate,
) )
} else { } else {
krilla::image::Image::from_custom(PdfImage::new(raster), interpolate) krilla::image::Image::from_custom(PdfRasterImage::new(raster), interpolate)
} }
} }
#[comemo::memoize]
fn convert_pdf(pdf: &PdfImage) -> PdfDocument {
PdfDocument::new(pdf.document().pdf().clone())
}
fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) { fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) {
let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| { let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| {
if hp { if hp {

View File

@ -49,7 +49,6 @@ pub(crate) fn handle_link(
fc.push_annotation( fc.push_annotation(
LinkAnnotation::new( LinkAnnotation::new(
rect, rect,
None,
Target::Action(Action::Link(LinkAction::new(u.to_string()))), Target::Action(Action::Link(LinkAction::new(u.to_string()))),
) )
.into(), .into(),
@ -64,7 +63,6 @@ pub(crate) fn handle_link(
fc.push_annotation( fc.push_annotation(
LinkAnnotation::new( LinkAnnotation::new(
rect, rect,
None,
Target::Destination(krilla::destination::Destination::Named( Target::Destination(krilla::destination::Destination::Named(
nd.clone(), nd.clone(),
)), )),
@ -83,7 +81,6 @@ pub(crate) fn handle_link(
fc.push_annotation( fc.push_annotation(
LinkAnnotation::new( LinkAnnotation::new(
rect, rect,
None,
Target::Destination(krilla::destination::Destination::Xyz( Target::Destination(krilla::destination::Destination::Xyz(
XyzDestination::new(index, pos.point.to_krilla()), XyzDestination::new(index, pos.point.to_krilla()),
)), )),

View File

@ -13,11 +13,13 @@ keywords = { workspace = true }
readme = { workspace = true } readme = { workspace = true }
[dependencies] [dependencies]
typst-assets = { workspace = true }
typst-library = { workspace = true } typst-library = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
hayro = { workspace = true }
image = { workspace = true } image = { workspace = true }
pixglyph = { workspace = true } pixglyph = { workspace = true }
resvg = { workspace = true } resvg = { workspace = true }

View File

@ -1,11 +1,12 @@
use std::sync::Arc; use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont};
use image::imageops::FilterType; use image::imageops::FilterType;
use image::{GenericImageView, Rgba}; use image::{GenericImageView, Rgba};
use std::sync::Arc;
use tiny_skia as sk; use tiny_skia as sk;
use tiny_skia::IntSize;
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::layout::Size; use typst_library::layout::Size;
use typst_library::visualize::{Image, ImageKind, ImageScaling}; use typst_library::visualize::{Image, ImageKind, ImageScaling, PdfImage};
use crate::{AbsExt, State}; use crate::{AbsExt, State};
@ -59,9 +60,9 @@ pub fn render_image(
/// Prepare a texture for an image at a scaled size. /// Prepare a texture for an image at a scaled size.
#[comemo::memoize] #[comemo::memoize]
fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> { fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let mut texture = sk::Pixmap::new(w, h)?; let texture = match image.kind() {
match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
let mut texture = sk::Pixmap::new(w, h)?;
let w = texture.width(); let w = texture.width();
let h = texture.height(); let h = texture.height();
@ -85,15 +86,63 @@ fn build_texture(image: &Image, w: u32, h: u32) -> Option<Arc<sk::Pixmap>> {
let Rgba([r, g, b, a]) = src; let Rgba([r, g, b, a]) = src;
*dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply();
} }
texture
} }
ImageKind::Svg(svg) => { ImageKind::Svg(svg) => {
let mut texture = sk::Pixmap::new(w, h)?;
let tree = svg.tree(); let tree = svg.tree();
let ts = tiny_skia::Transform::from_scale( let ts = tiny_skia::Transform::from_scale(
w as f32 / tree.size().width(), w as f32 / tree.size().width(),
h as f32 / tree.size().height(), h as f32 / tree.size().height(),
); );
resvg::render(tree, ts, &mut texture.as_mut()); resvg::render(tree, ts, &mut texture.as_mut());
texture
} }
} ImageKind::Pdf(pdf) => build_pdf_texture(pdf, w, h)?,
};
Some(Arc::new(texture)) Some(Arc::new(texture))
} }
// Keep this in sync with `typst-svg`!
fn build_pdf_texture(pdf: &PdfImage, w: u32, h: u32) -> Option<sk::Pixmap> {
let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> {
let bytes = match font {
StandardFont::Helvetica => typst_assets::pdf::SANS,
StandardFont::HelveticaBold => typst_assets::pdf::SANS_BOLD,
StandardFont::HelveticaOblique => typst_assets::pdf::SANS_ITALIC,
StandardFont::HelveticaBoldOblique => typst_assets::pdf::SANS_BOLD_ITALIC,
StandardFont::Courier => typst_assets::pdf::FIXED,
StandardFont::CourierBold => typst_assets::pdf::FIXED_BOLD,
StandardFont::CourierOblique => typst_assets::pdf::FIXED_ITALIC,
StandardFont::CourierBoldOblique => typst_assets::pdf::FIXED_BOLD_ITALIC,
StandardFont::TimesRoman => typst_assets::pdf::SERIF,
StandardFont::TimesBold => typst_assets::pdf::SERIF_BOLD,
StandardFont::TimesItalic => typst_assets::pdf::SERIF_ITALIC,
StandardFont::TimesBoldItalic => typst_assets::pdf::SERIF_BOLD_ITALIC,
StandardFont::ZapfDingBats => typst_assets::pdf::DING_BATS,
StandardFont::Symbol => typst_assets::pdf::SYMBOL,
};
Some((Arc::new(bytes), 0))
};
let interpreter_settings = InterpreterSettings {
font_resolver: Arc::new(move |query| match query {
FontQuery::Standard(s) => select_standard_font(*s),
FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()),
}),
warning_sink: Arc::new(|_| {}),
};
let render_settings = RenderSettings {
x_scale: w as f32 / pdf.width(),
y_scale: h as f32 / pdf.height(),
width: Some(w as u16),
height: Some(h as u16),
};
let hayro_pix = hayro::render(pdf.page(), &interpreter_settings, &render_settings);
sk::Pixmap::from_vec(hayro_pix.take_u8(), IntSize::from_wh(w, h)?)
}

View File

@ -13,6 +13,7 @@ keywords = { workspace = true }
readme = { workspace = true } readme = { workspace = true }
[dependencies] [dependencies]
typst-assets = { workspace = true }
typst-library = { workspace = true } typst-library = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }
typst-timing = { workspace = true } typst-timing = { workspace = true }
@ -21,6 +22,7 @@ base64 = { workspace = true }
comemo = { workspace = true } comemo = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
flate2 = { workspace = true } flate2 = { workspace = true }
hayro = { workspace = true }
image = { workspace = true } image = { workspace = true }
ttf-parser = { workspace = true } ttf-parser = { workspace = true }
xmlparser = { workspace = true } xmlparser = { workspace = true }

View File

@ -1,10 +1,13 @@
use std::sync::Arc;
use base64::Engine; use base64::Engine;
use ecow::{EcoString, eco_format}; use ecow::{EcoString, eco_format};
use hayro::{FontData, FontQuery, InterpreterSettings, RenderSettings, StandardFont};
use image::{ImageEncoder, codecs::png::PngEncoder}; use image::{ImageEncoder, codecs::png::PngEncoder};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
use typst_library::layout::{Abs, Axes}; use typst_library::layout::{Abs, Axes};
use typst_library::visualize::{ use typst_library::visualize::{
ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, ExchangeFormat, Image, ImageKind, ImageScaling, PdfImage, RasterFormat,
}; };
use crate::SVGRenderer; use crate::SVGRenderer;
@ -66,6 +69,25 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
}), }),
}, },
ImageKind::Svg(svg) => ("svg+xml", svg.data()), ImageKind::Svg(svg) => ("svg+xml", svg.data()),
ImageKind::Pdf(pdf) => {
// To make sure the image isn't pixelated, we always scale up so the
// lowest dimension has at least 1000 pixels. However, we only scale
// up as much so that the largest dimension doesn't exceed 3000
// pixels.
const MIN_RES: f32 = 1000.0;
const MAX_RES: f32 = 3000.0;
let base_width = pdf.width();
let w_scale = (MIN_RES / base_width).max(MAX_RES / base_width);
let base_height = pdf.height();
let h_scale = (MIN_RES / base_height).min(MAX_RES / base_height);
let total_scale = w_scale.min(h_scale);
let width = (base_width * total_scale).ceil() as u32;
let height = (base_height * total_scale).ceil() as u32;
buf = pdf_to_png(pdf, width, height);
("png", buf.as_slice())
}
}; };
let mut url = eco_format!("data:image/{format};base64,"); let mut url = eco_format!("data:image/{format};base64,");
@ -73,3 +95,45 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString {
url.push_str(&data); url.push_str(&data);
url url
} }
// Keep this in sync with `typst-png`!
fn pdf_to_png(pdf: &PdfImage, w: u32, h: u32) -> Vec<u8> {
let select_standard_font = move |font: StandardFont| -> Option<(FontData, u32)> {
let bytes = match font {
StandardFont::Helvetica => typst_assets::pdf::SANS,
StandardFont::HelveticaBold => typst_assets::pdf::SANS_BOLD,
StandardFont::HelveticaOblique => typst_assets::pdf::SANS_ITALIC,
StandardFont::HelveticaBoldOblique => typst_assets::pdf::SANS_BOLD_ITALIC,
StandardFont::Courier => typst_assets::pdf::FIXED,
StandardFont::CourierBold => typst_assets::pdf::FIXED_BOLD,
StandardFont::CourierOblique => typst_assets::pdf::FIXED_ITALIC,
StandardFont::CourierBoldOblique => typst_assets::pdf::FIXED_BOLD_ITALIC,
StandardFont::TimesRoman => typst_assets::pdf::SERIF,
StandardFont::TimesBold => typst_assets::pdf::SERIF_BOLD,
StandardFont::TimesItalic => typst_assets::pdf::SERIF_ITALIC,
StandardFont::TimesBoldItalic => typst_assets::pdf::SERIF_BOLD_ITALIC,
StandardFont::ZapfDingBats => typst_assets::pdf::DING_BATS,
StandardFont::Symbol => typst_assets::pdf::SYMBOL,
};
Some((Arc::new(bytes), 0))
};
let interpreter_settings = InterpreterSettings {
font_resolver: Arc::new(move |query| match query {
FontQuery::Standard(s) => select_standard_font(*s),
FontQuery::Fallback(f) => select_standard_font(f.pick_standard_font()),
}),
warning_sink: Arc::new(|_| {}),
};
let render_settings = RenderSettings {
x_scale: w as f32 / pdf.width(),
y_scale: h as f32 / pdf.height(),
width: Some(w as u16),
height: Some(h as u16),
};
let hayro_pix = hayro::render(pdf.page(), &interpreter_settings, &render_settings);
hayro_pix.take_png()
}

View File

@ -665,12 +665,3 @@ applicable, contains possible workarounds.
[`page` function]($page) which will force a page break. If you just want a few [`page` function]($page) which will force a page break. If you just want a few
paragraphs to stretch into the margins, then reverting to the old margins, you paragraphs to stretch into the margins, then reverting to the old margins, you
can use the [`pad` function]($pad) with negative padding. can use the [`pad` function]($pad) with negative padding.
- **Include PDFs as images.** In LaTeX, it has become customary to insert vector
graphics as PDF or EPS files. Typst supports neither format as an image
format, but you can easily convert both into SVG files with [online
tools](https://cloudconvert.com/pdf-to-svg) or
[Inkscape](https://inkscape.org/). The web app will automatically convert PDF
files to SVG files upon uploading them. You can also use the
community-provided [`muchpdf` package](https://typst.app/universe/package/muchpdf)
to embed PDFs. It internally converts PDFs to SVGs on-the-fly.

View File

@ -84,8 +84,9 @@ meaning in Typst. We can use `=`, `-`, `+`, and `_` to create headings, lists
and emphasized text, respectively. However, having a special symbol for and emphasized text, respectively. However, having a special symbol for
everything we want to insert into our document would soon become cryptic and everything we want to insert into our document would soon become cryptic and
unwieldy. For this reason, Typst reserves markup symbols only for the most unwieldy. For this reason, Typst reserves markup symbols only for the most
common things. Everything else is inserted with _functions._ For our image to common things. Everything else is inserted with _functions._ For
show up on the page, we use Typst's [`image`] function. [our image](https://github.com/typst/typst-dev-assets/blob/main/files/images/glacier.jpg)
to show up on the page, we use Typst's [`image`] function.
```example ```example
#image("glacier.jpg") #image("glacier.jpg")
@ -125,19 +126,38 @@ mode. This means, you now have to remove the hash before the image function call
The hash is only needed directly in markup (to disambiguate text from function The hash is only needed directly in markup (to disambiguate text from function
calls). calls).
The caption consists of arbitrary markup. To give markup to a function, we The caption consists of arbitrary markup, and can also be a string. To give
enclose it in square brackets. This construct is called a _content block._ markup to a function, we enclose it in square brackets. This construct is called
a _content block._
```example ```example
#figure( #figure(
image("glacier.jpg", width: 70%), image("glacier.jpg", width: 70%),
caption: [ caption: box[
_Glaciers_ form an important part _Glaciers_ form an important part
of the earth's climate system. of the earth's climate system.
], ],
) )
``` ```
**Be careful** about putting the square brackets by themselves on separate
lines. This will introduce leading and trailing space around inline text inside
the brackets, that is hard to notice. Below are several caption examples: one
with extra undesired space, and 3 correct ones.
```example
#show rect: none
#figure(rect(), caption: [
Caption text
])
#figure(rect(), caption: box[
Caption text
])
#figure(rect(), caption: [Caption
text]) // Many spaces in markup counts as one.
#figure(rect(), caption: "Caption text") // Spaces in strings are displayed verbatim.
```
You continue to write your report and now want to reference the figure. To do You continue to write your report and now want to reference the figure. To do
that, first attach a label to figure. A label uniquely identifies an element in that, first attach a label to figure. A label uniquely identifies an element in
your document. Add one after the figure by enclosing some name in angle your document. Add one after the figure by enclosing some name in angle

View File

@ -110,8 +110,8 @@ font. For the purposes of the example, we'll also set another page size.
margin: (x: 1.8cm, y: 1.5cm), margin: (x: 1.8cm, y: 1.5cm),
) )
#set text( #set text(
size: 10pt,
font: "New Computer Modern", font: "New Computer Modern",
size: 10pt
) )
#set par( #set par(
justify: true, justify: true,
@ -235,19 +235,27 @@ Instead, you could maybe
[define a custom function]($function/#defining-functions) that always yields the [define a custom function]($function/#defining-functions) that always yields the
logo with its image. However, there is an even easier way: logo with its image. However, there is an even easier way:
With show rules, you can redefine how Typst displays certain elements. You With show rules, you can redefine how Typst displays certain elements. You can
specify which elements Typst should show differently and how they should look. specify which elements Typst should show differently and how they should look.
Show rules can be applied to instances of text, many functions, and even the Show rules can be applied to instances of text, many functions, and even the
whole document. whole document.
```example ```example
#show "ArtosFlow": name => box[ // #show "ArtosFlow": name => {
#box(image( // let logo = box(image(
// "logo.svg",
// height: 0.7em,
// ))
// [#logo #name]
// }
#show "ArtosFlow": name => {
box(image(
"logo.svg", "logo.svg",
height: 0.7em, height: 0.7em,
)) ))
#name " "
] name
}
This report is embedded in the This report is embedded in the
ArtosFlow project. ArtosFlow is a ArtosFlow project. ArtosFlow is a
@ -256,18 +264,18 @@ project of the Artos Institute.
There is a lot of new syntax in this example: We write the `{show}` keyword, There is a lot of new syntax in this example: We write the `{show}` keyword,
followed by a string of text we want to show differently and a colon. Then, we followed by a string of text we want to show differently and a colon. Then, we
write a function that takes the content that shall be shown as an argument. write a function that takes the content as an argument that shall be shown.
Here, we called that argument `name`. We can now use the `name` variable in the Here, we called that argument `name`. We can now use the `name` variable in
function's body to print the ArtosFlow name. Our show rule adds the logo image the function's body to display the ArtosFlow name. Our show rule adds the logo
in front of the name and puts the result into a box to prevent linebreaks from image to the left of the name and inserts a single space between the two.
occurring between logo and name. The image is also put inside of a box, so that The image is put inside of a `box`, so that it does not appear in its own
it does not appear in its own paragraph. paragraph, because `image` is a block-level element.
The calls to the first box function and the image function did not require a <!-- The calls to the first box function and the image function did not require a -->
leading `#` because they were not embedded directly in markup. When Typst <!-- leading `#` because they were not embedded directly in markup. When Typst -->
expects code instead of markup, the leading `#` is not needed to access <!-- expects code instead of markup, the leading `#` is not needed to access -->
functions, keywords, and variables. This can be observed in parameter lists, <!-- functions, keywords, and variables. This can be observed in parameter lists, -->
function definitions, and [code blocks]($scripting). <!-- function definitions, and [code blocks]($scripting). -->
## Review ## Review
You now know how to apply basic formatting to your Typst documents. You learned You now know how to apply basic formatting to your Typst documents. You learned

View File

@ -6,7 +6,7 @@ description: Typst's tutorial.
In the previous two chapters of this tutorial, you have learned how to write a In the previous two chapters of this tutorial, you have learned how to write a
document in Typst and how to change its formatting. The report you wrote document in Typst and how to change its formatting. The report you wrote
throughout the last two chapters got a straight A and your supervisor wants to throughout the last two chapters got a straight A and your supervisor wants to
base a conference paper on it! The report will of course have to comply with the base a conference paper on it! The paper will of course have to comply with the
conference's style guide. Let's see how we can achieve that. conference's style guide. Let's see how we can achieve that.
Before we start, let's create a team, invite your supervisor and add them to the Before we start, let's create a team, invite your supervisor and add them to the
@ -30,12 +30,12 @@ to find other users and try teams with them!
The layout guidelines are available on the conference website. Let's take a look The layout guidelines are available on the conference website. Let's take a look
at them: at them:
- The font should be an 11pt serif font - The font should be an 11 pt serif font
- The title should be in 17pt and bold - The title should be in 17 pt and bold
- The paper contains a single-column abstract and two-column main text - The paper contains a single-column abstract and two-column main text
- The abstract should be centered - The abstract should be centered
- The main text should be justified - The main text should be justified
- First level section headings should be 13pt, centered, and rendered in small - First level section headings should be 13 pt, centered, and rendered in small
capitals capitals
- Second level headings are run-ins, italicized and have the same size as the - Second level headings are run-ins, italicized and have the same size as the
body text body text
@ -51,7 +51,6 @@ Let's start by writing some set rules for the document.
```example ```example
#set page( #set page(
>>> margin: auto,
paper: "us-letter", paper: "us-letter",
header: align(right)[ header: align(right)[
A fluid dynamic model for A fluid dynamic model for
@ -61,8 +60,8 @@ Let's start by writing some set rules for the document.
) )
#set par(justify: true) #set par(justify: true)
#set text( #set text(
font: "Libertinus Serif",
size: 11pt, size: 11pt,
font: "Libertinus Serif",
) )
#lorem(600) #lorem(600)
@ -72,12 +71,12 @@ You are already familiar with most of what is going on here. We set the text
size to `{11pt}` and the font to Libertinus Serif. We also enable paragraph size to `{11pt}` and the font to Libertinus Serif. We also enable paragraph
justification and set the page size to US letter. justification and set the page size to US letter.
The `header` argument is new: With it, we can provide content to fill the top The `header` field is new: with it, we can provide content to fill the top
margin of every page. In the header, we specify our paper's title as requested margin of every page. In the header, we specify our paper's title as requested
by the conference style guide. We use the `align` function to align the text to by the conference style guide. We use the `align` function to align the text to
the right. the right.
Last but not least is the `numbering` argument. Here, we can provide a Last but not least is the `numbering` field. Here, we can provide a
[numbering pattern]($numbering) that defines how to number the pages. By [numbering pattern]($numbering) that defines how to number the pages. By
setting it to `{"1"}`, Typst only displays the bare page number. Setting it to setting it to `{"1"}`, Typst only displays the bare page number. Setting it to
`{"(1/1)"}` would have displayed the current page and total number of pages `{"(1/1)"}` would have displayed the current page and total number of pages
@ -89,35 +88,65 @@ Now, let's add a title and an abstract. We'll start with the title. We center
align it and increase its font weight by enclosing it in `[*stars*]`. align it and increase its font weight by enclosing it in `[*stars*]`.
```example ```example
>>> #set page(width: 300pt, margin: 30pt) >>> #set page(
>>> #set text(font: "Libertinus Serif", 11pt) >>> // paper: "us-letter",
#align(center, text(17pt)[ >>> width: 300pt,
>>> margin: 30pt,
>>> header: align(right)[
>>> A fluid dynamic model for
>>> glacier flow
>>> ],
>>> // numbering: "1",
>>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#align(center, block(text(
17pt,
hyphenate: false
)[
*A fluid dynamic model *A fluid dynamic model
for glacier flow* for glacier flow*
]) ]))
``` ```
This looks right. We used the `text` function to override the previous text This looks right. We used the `text` function to override the previous text
set rule locally, increasing the size to 17pt for the function's argument. Let's set rule locally, increasing the size to 17 pt.
also add the author list: Since we are writing this paper together with our Add explanation about block+hyphenate, which is pretty convoluted.
supervisor, we'll add our own and their name. Let's also add the author list: Since we are writing this paper together with
our supervisor, we'll add our own and their name.
```example ```example
>>> #set page(width: 300pt, margin: 30pt) >>> #set page(
>>> #set text(font: "Libertinus Serif", 11pt) >>> // paper: "us-letter",
>>> width: 300pt,
>>> margin: 30pt,
>>> header: align(right)[
>>> A fluid dynamic model for
>>> glacier flow
>>> ],
>>> // numbering: "1",
>>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
>>> >>>
>>> #align(center, text(17pt)[ >>> #align(center, block(text(
>>> 17pt,
>>> hyphenate: false
>>> )[
>>> *A fluid dynamic model >>> *A fluid dynamic model
>>> for glacier flow* >>> for glacier flow*
>>> ]) >>> ]))
<<< ...
#grid( #grid(
columns: (1fr, 1fr), columns: (1fr, 1fr),
align(center)[ align: center,
[
Therese Tungsten \ Therese Tungsten \
Artos Institute \ Artos Institute \
#link("mailto:tung@artos.edu") #link("mailto:tung@artos.edu")
], ],
align(center)[ [
Dr. John Doe \ Dr. John Doe \
Artos Institute \ Artos Institute \
#link("mailto:doe@artos.edu") #link("mailto:doe@artos.edu")
@ -127,45 +156,49 @@ supervisor, we'll add our own and their name.
The two author blocks are laid out next to each other. We use the [`grid`] The two author blocks are laid out next to each other. We use the [`grid`]
function to create this layout. With a grid, we can control exactly how large function to create this layout. With a grid, we can control exactly how large
each column is and which content goes into which cell. The `columns` argument each column is and which content goes into which cell. The `columns` field
takes an array of [relative lengths]($relative) or [fractions]($fraction). In takes the number of columns, or an array of [relative lengths]($relative) or
this case, we passed it two equal fractional sizes, telling it to split the [fractions]($fraction). In this case, we passed it two equal fractional sizes,
available space into two equal columns. We then passed two content arguments to telling it to split the available space into two equal columns. We then passed
the grid function. The first with our own details, and the second with our two content arguments to the grid function --- the first with our own details,
supervisors'. We again use the `align` function to center the content within the and the second with our supervisor's. With grid, we can avoid using `align` on
column. The grid takes an arbitrary number of content arguments specifying the each cell content to center them, and instead use the `align` field to do this
cells. Rows are added automatically, but they can also be manually sized with for all cells automatically. The grid takes an arbitrary number of content
the `rows` argument. arguments specifying the cells. Rows are added automatically, but they can also
be manually sized with the `rows` field.
Now, let's add the abstract. Remember that the conference wants the abstract to Now, let's add the abstract. Remember that the conference wants the abstract to
be set ragged and centered. be set ragged and centered.
```example:0,0,612,317.5 ```example:0,0,612,317.5
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page( >>> #set page(
>>> "us-letter", >>> paper: "us-letter",
>>> margin: auto, >>> header: align(right)[
>>> header: align(right + horizon)[
>>> A fluid dynamic model for >>> A fluid dynamic model for
>>> glacier flow >>> glacier flow
>>> ], >>> ],
>>> numbering: "1", >>> numbering: "1",
>>> ) >>> )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
>>> >>>
>>> #align(center, text(17pt)[ >>> #align(center, block(text(
>>> 17pt,
>>> hyphenate: false
>>> )[
>>> *A fluid dynamic model >>> *A fluid dynamic model
>>> for glacier flow* >>> for glacier flow*
>>> ]) >>> ]))
>>> >>>
>>> #grid( >>> #grid(
>>> columns: (1fr, 1fr), >>> columns: (1fr, 1fr),
>>> align(center)[ >>> align: center,
>>> [
>>> Therese Tungsten \ >>> Therese Tungsten \
>>> Artos Institute \ >>> Artos Institute \
>>> #link("mailto:tung@artos.edu") >>> #link("mailto:tung@artos.edu")
>>> ], >>> ],
>>> align(center)[ >>> [
>>> Dr. John Doe \ >>> Dr. John Doe \
>>> Artos Institute \ >>> Artos Institute \
>>> #link("mailto:doe@artos.edu") >>> #link("mailto:doe@artos.edu")
@ -198,35 +231,33 @@ keyword:
for glacier flow for glacier flow
] ]
<<< ...
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
#set page( #set page(
>>> "us-letter", >>> paper: "us-letter",
>>> margin: auto, <<< ...
header: align( header: align(right, title),
right + horizon,
title
),
<<< ... <<< ...
>>> numbering: "1", >>> numbering: "1",
) )
>>> #set par(justify: true)
#align(center, text(17pt)[ >>> #set text(11pt, font: "Libertinus Serif")
*#title*
])
<<< ... <<< ...
#align(center, block(text(
17pt,
hyphenate: false,
strong(title),
)))
<<< ...
>>> #grid( >>> #grid(
>>> columns: (1fr, 1fr), >>> columns: (1fr, 1fr),
>>> align(center)[ >>> align: center,
>>> [
>>> Therese Tungsten \ >>> Therese Tungsten \
>>> Artos Institute \ >>> Artos Institute \
>>> #link("mailto:tung@artos.edu") >>> #link("mailto:tung@artos.edu")
>>> ], >>> ],
>>> align(center)[ >>> [
>>> Dr. John Doe \ >>> Dr. John Doe \
>>> Artos Institute \ >>> Artos Institute \
>>> #link("mailto:doe@artos.edu") >>> #link("mailto:doe@artos.edu")
@ -247,55 +278,48 @@ and also within markup (prefixed by `#`, like functions). This way, if we decide
on another title, we can easily change it in one place. on another title, we can easily change it in one place.
## Adding columns and headings { #columns-and-headings } ## Adding columns and headings { #columns-and-headings }
The paper above unfortunately looks like a wall of lead. To fix that, let's add The paper above unfortunately looks like a wall of lead(?). To fix that, let's add
some headings and switch our paper to a two-column layout. Fortunately, that's some headings and switch our paper to a two-column layout. Fortunately, that's
easy to do: We just need to amend our `page` set rule with the `columns` easy to do: we just need to amend our `page` set rule with the `columns` field.
argument.
By adding `{columns: 2}` to the argument list, we have wrapped the whole By adding `{columns: 2}` to the argument list, we have wrapped the whole
document in two columns. However, that would also affect the title and authors document in two columns. However, that would also affect the title and authors
overview. To keep them spanning the whole page, we can wrap them in a function overview. To keep them spanning the whole page, we can wrap them in a function
call to [`{place}`]($place). Place expects an alignment and the content it call to [`{place}`]($place). The `place` expects an alignment and the content it
should place as positional arguments. Using the named `{scope}` argument, we can should place as positional arguments. Using the named `scope` field, we can
decide if the items should be placed relative to the current column or its decide if the items should be placed relative to the current column or its
parent (the page). There is one more thing to configure: If no other arguments parent (the page). There is one more thing to configure: If no other arguments
are provided, `{place}` takes its content out of the flow of the document and are provided, `place` takes its content out of the flow of the document and
positions it over the other content without affecting the layout of other positions it over the other content without affecting the layout of other
content in its container: content in its container:
```example ```example
#place( #place(top + center, rect())
top + center,
rect(fill: black),
)
#lorem(30) #lorem(30)
``` ```
If we hadn't used `{place}` here, the square would be in its own line, but here If we hadn't used `place` here, the rectangle would be in its own line, but
it overlaps the few lines of text following it. Likewise, that text acts like as here it overlaps the few lines of text following it. Likewise, that text acts
if there was no square. To change this behavior, we can pass the argument like as if there was no rectangle. To change this behavior, we can pass the
`{float: true}` to ensure that the space taken up by the placed item at the top argument `{float: true}` to ensure that the space taken up by the placed item
or bottom of the page is not occupied by any other content. at the top or bottom of the page is not occupied by any other content.
```example:single ```example:single
>>> #let title = [ >>> #let title = [
>>> A fluid dynamic model >>> A fluid dynamic model
>>> for glacier flow >>> for glacier flow
>>> ] >>> ]
>>> <<< ...
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>>
#set page( #set page(
>>> margin: auto,
paper: "us-letter", paper: "us-letter",
header: align( header: align(right, title),
right + horizon,
title
),
numbering: "1", numbering: "1",
columns: 2, columns: 2,
) )
>>> #set par(justify: true)
>>> #set text(11pt, font: "Libertinus Serif")
<<< ...
#place( #place(
top + center, top + center,
@ -303,11 +327,7 @@ or bottom of the page is not occupied by any other content.
scope: "parent", scope: "parent",
clearance: 2em, clearance: 2em,
)[ )[
>>> #text( >>> #block(text(17pt, hyphenate: false, strong(title)))
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> )
>>> >>>
>>> #grid( >>> #grid(
>>> columns: (1fr, 1fr), >>> columns: (1fr, 1fr),
@ -324,10 +344,9 @@ or bottom of the page is not occupied by any other content.
>>> ) >>> )
<<< ... <<< ...
#par(justify: false)[ #set par(justify: false) // Put it above to remove hyphenate?
*Abstract* \ *Abstract* \
#lorem(80) #lorem(80)
]
] ]
= Introduction = Introduction
@ -337,14 +356,14 @@ or bottom of the page is not occupied by any other content.
#lorem(200) #lorem(200)
``` ```
In this example, we also used the `clearance` argument of the `{place}` function In this example, we also used the `clearance` argument of the `place` function
to provide the space between it and the body instead of using the [`{v}`]($v) to provide the space between it and the body instead of using the [`v`]
function. We can also remove the explicit `{align(center, ..)}` calls around the function. We can also remove the explicit `{align(center, ..)}` calls around the
various parts since they inherit the center alignment from the placement. various parts since they inherit the center alignment from the placement.
Now there is only one thing left to do: Style our headings. We need to make them Now there is only one thing left to do: style our headings. We need to make them
centered and use small capitals. Because the `heading` function does not offer centered and use small capitals. For centering we can use a show-set rule, but
a way to set any of that, we need to write our own heading show rule. to use small capitals we need to write our own heading show rule.
```example:50,250,265,270 ```example:50,250,265,270
>>> #let title = [ >>> #let title = [
@ -352,37 +371,27 @@ a way to set any of that, we need to write our own heading show rule.
>>> for glacier flow >>> for glacier flow
>>> ] >>> ]
>>> >>>
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page( >>> #set page(
>>> "us-letter", >>> paper: "us-letter",
>>> margin: auto, >>> header: align(right, title),
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> numbering: "1", >>> numbering: "1",
>>> columns: 2, >>> columns: 2,
>>> ) >>> )
#show heading: it => [ >>> #set par(justify: true)
#set align(center) >>> #set text(11pt, font: "Libertinus Serif")
#set text(13pt, weight: "regular") <<< ...
#block(smallcaps(it.body)) #show heading: set align(center)
] #show heading: set text(13pt, weight: "regular")
#show heading: it => block(smallcaps(it.body))
<<< ... <<< ...
>>>
>>> #place( >>> #place(
>>> top + center, >>> top + center,
>>> float: true, >>> float: true,
>>> scope: "parent", >>> scope: "parent",
>>> clearance: 2em, >>> clearance: 2em,
>>> )[ >>> )[
>>> #text( >>> #block(text(17pt, hyphenate: false, strong(title)))
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> )
>>> >>>
>>> #grid( >>> #grid(
>>> columns: (1fr, 1fr), >>> columns: (1fr, 1fr),
@ -398,11 +407,10 @@ a way to set any of that, we need to write our own heading show rule.
>>> ] >>> ]
>>> ) >>> )
>>> >>>
>>> #par(justify: false)[ >>> #set par(justify: false)
>>> *Abstract* \ >>> *Abstract* \
>>> #lorem(80) >>> #lorem(80)
>>> ] >>> ]
>>> ]
>>> >>>
>>> = Introduction >>> = Introduction
>>> #lorem(35) >>> #lorem(35)
@ -411,19 +419,26 @@ a way to set any of that, we need to write our own heading show rule.
>>> #lorem(45) >>> #lorem(45)
``` ```
This looks great! We used a show rule that applies to all headings. We give it a This looks great! We used a few rules that apply to all headings. First, we
function that gets passed the heading as a parameter. That parameter can be used made headings centered, then we set font size to 13 pt and removed default
as content but it also has some fields like `title`, `numbers`, and `level` from heading boldness by setting `weight` to `{"regular"}`. Lastly, there is a show
which we can compose a custom look. Here, we are center-aligning, setting the rule with a closure, i.e., a callback function. We gave it a function that
font weight to `{"regular"}` because headings are bold by default, and use the passes the heading as argument. That argument can be used as content but it
[`smallcaps`] function to render the heading's title in small capitals. also has some fields like `title`, `numbers`, and `level`, from which we can
compose a custom look. Here, we use the [`smallcaps`] function to render the
heading's title in small capitals. Note that heading itself is wrapped in a
block by default, as it's a block-level element, and by using `{it.body}` we
are destroying the default structure of the heading. This means we strip away
not only the `block` "shell", but also any potential numbering and other
heading features. To restore it's semantic structure, we wrap
`{smallcaps(it.body)}` in a `block`. This way it will behave like usual.
The only remaining problem is that all headings look the same now. The The only remaining problem is that all headings now look the same. The
"Motivation" and "Problem Statement" subsections ought to be italic run in "Motivation" and "Problem Statement" subsections ought to be italic run-in
headers, but right now, they look indistinguishable from the section headings. We headings, but right now, they look indistinguishable from the section headings.
can fix that by using a `where` selector on our set rule: This is a We can fix that by using a `where` selector on our show rules: this is a
[method]($scripting/#methods) we can call on headings (and other [method]($scripting/#methods) we can call on headings (and other elements) that
elements) that allows us to filter them by their level. We can use it to allows us to filter them by their level (and other fields). We can use it to
differentiate between section and subsection headings: differentiate between section and subsection headings:
```example:50,250,265,245 ```example:50,250,265,245
@ -432,47 +447,31 @@ differentiate between section and subsection headings:
>>> for glacier flow >>> for glacier flow
>>> ] >>> ]
>>> >>>
>>> #set text(font: "Libertinus Serif", 11pt)
>>> #set par(justify: true)
>>> #set page( >>> #set page(
>>> "us-letter", >>> paper: "us-letter",
>>> margin: auto, >>> header: align(right, title),
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> numbering: "1", >>> numbering: "1",
>>> columns: 2, >>> columns: 2,
>>> ) >>> )
>>> >>> #set par(justify: true)
#show heading.where( >>> #set text(11pt, font: "Libertinus Serif")
level: 1 <<< ...
): it => block(width: 100%)[
#set align(center)
#set text(13pt, weight: "regular")
#smallcaps(it.body)
]
#show heading.where( #show heading.where(level: 1): set align(center)
level: 2 #show heading.where(level: 1): set text(13pt, weight: "regular")
): it => text( #show heading.where(level: 1): it => block(smallcaps(it.body))
size: 11pt,
weight: "regular", #show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
style: "italic", #show heading.where(level: 2): it => [#it.body.]
it.body + [.],
) <<< ...
>>>
>>> #place( >>> #place(
>>> top + center, >>> top + center,
>>> float: true, >>> float: true,
>>> scope: "parent", >>> scope: "parent",
>>> clearance: 2em, >>> clearance: 2em,
>>> )[ >>> )[
>>> #text( >>> #block(text(17pt, hyphenate: false, strong(title)))
>>> 17pt,
>>> weight: "bold",
>>> title,
>>> )
>>> >>>
>>> #grid( >>> #grid(
>>> columns: (1fr, 1fr), >>> columns: (1fr, 1fr),
@ -488,11 +487,10 @@ differentiate between section and subsection headings:
>>> ] >>> ]
>>> ) >>> )
>>> >>>
>>> #par(justify: false)[ >>> #set par(justify: false)
>>> *Abstract* \ >>> *Abstract* \
>>> #lorem(80) >>> #lorem(80)
>>> ] >>> ]
>>> ]
>>> >>>
>>> = Introduction >>> = Introduction
>>> #lorem(35) >>> #lorem(35)
@ -501,23 +499,26 @@ differentiate between section and subsection headings:
>>> #lorem(45) >>> #lorem(45)
``` ```
This looks great! We wrote two show rules that each selectively apply to the Excellent! We wrote several rules that selectively apply to the first and second
first and second level headings. We used a `where` selector to filter the level headings. We used a `where` selector to filter the headings by their
headings by their level. We then rendered the subsection headings as run-ins. We level. We then rendered the subsection headings as run-ins. We also
also automatically add a period to the end of the subsection headings. automatically added a period to the end of the subsection headings. This time
we did not wrap result in a `block`, because we need the heading to be inline
with the following text.
Let's review the conference's style guide: Let's review the conference's style guide:
- The font should be an 11pt serif font ✓ - The font should be an 11 pt serif font ✓
- The title should be in 17pt and bold ✓ - The title should be in 17 pt and bold ✓
- The paper contains a single-column abstract and two-column main text ✓ - The paper contains a single-column abstract and two-column main text ✓
- The abstract should be centered ✓ - The abstract should be centered ✓
- The main text should be justified ✓ - The main text should be justified ✓
- First level section headings should be centered, rendered in small caps and in - First level section headings should be centered, rendered in small caps and in
13pt ✓ 13 pt ✓
- Second level headings are run-ins, italicized and have the same size as the - Second level headings are run-ins, italicized and have the same size as the
body text ✓ body text ✓
- Finally, the pages should be US letter sized, numbered in the center and the - Finally, the pages should be US letter sized, numbered in the center of the
top right corner of each page should contain the title of the paper ✓ footer and the top right corner of each page should contain the title of the
paper ✓
We are now in compliance with all of these styles and can submit the paper to We are now in compliance with all of these styles and can submit the paper to
the conference! The finished paper looks like this: the conference! The finished paper looks like this:
@ -528,6 +529,62 @@ the conference! The finished paper looks like this:
style="box-shadow: 0 4px 12px rgb(89 85 101 / 20%); width: 500px; max-width: 100%; display: block; margin: 24px auto;" style="box-shadow: 0 4px 12px rgb(89 85 101 / 20%); width: 500px; max-width: 100%; display: block; margin: 24px auto;"
> >
Here is a full listing of the finished paper:
```example
#let title = [A fluid dynamic model for glacier flow]
#set page(
paper: "us-letter",
header: align(right, title),
numbering: "1",
columns: 2,
)
#set par(justify: true)
#set text(11pt, font: "Libertinus Serif")
#show heading.where(level: 1): set align(center)
#show heading.where(level: 1): set text(13pt, weight: "regular")
#show heading.where(level: 1): it => block(smallcaps(it.body))
#show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
#show heading.where(level: 2): it => [#it.body.]
#place(top + center, float: true, scope: "parent", clearance: 2em)[
#block(text(17pt, hyphenate: false, strong(title)))
#grid(
columns: (1fr, 1fr),
[
Therese Tungsten \
Artos Institute \
#link("mailto:tung@artos.edu")
],
[
Dr. John Doe \
Artos Institute \
#link("mailto:doe@artos.edu")
]
)
#set par(justify: false)
*Abstract* \
#lorem(80)
]
= Introduction
#lorem(90)
== Motivation
#lorem(140)
== Problem Statement
#lorem(50)
= Related Work
#lorem(200)
```
## Review ## Review
You have now learned how to create headers and footers, how to use functions and You have now learned how to create headers and footers, how to use functions and
scopes to locally override styles, how to create more complex layouts with the scopes to locally override styles, how to create more complex layouts with the

View File

@ -25,7 +25,10 @@ You are #amazed[beautiful]!
This function takes a single argument, `term`, and returns a content block with This function takes a single argument, `term`, and returns a content block with
the `term` surrounded by sparkles. We also put the whole thing in a box so that the `term` surrounded by sparkles. We also put the whole thing in a box so that
the term we are amazed by cannot be separated from its sparkles by a line break. the term we are amazed by cannot be separated from its sparkles by a line
break. Alternatively, you can use a
[shorthand](https://typst.app/docs/reference/symbols/#shorthands)
for a no-break space and write `{[✨~#term~✨]}`.
Many functions that come with Typst have optional named parameters. Our Many functions that come with Typst have optional named parameters. Our
functions can also have them. Let's add a parameter to our function that lets us functions can also have them. Let's add a parameter to our function that lets us
@ -34,7 +37,7 @@ parameter isn't given.
```example ```example
#let amazed(term, color: blue) = { #let amazed(term, color: blue) = {
text(color, box[✨ #term ✨]) text(color)[✨~#term~✨]
} }
You are #amazed[beautiful]! You are #amazed[beautiful]!
@ -43,7 +46,7 @@ I am #amazed(color: purple)[amazed]!
Templates now work by wrapping our whole document in a custom function like Templates now work by wrapping our whole document in a custom function like
`amazed`. But wrapping a whole document in a giant function call would be `amazed`. But wrapping a whole document in a giant function call would be
cumbersome! Instead, we can use an "everything" show rule to achieve the same cumbersome! Instead, we can use an "global" show rule to achieve the same
with cleaner code. To write such a show rule, put a colon directly after the with cleaner code. To write such a show rule, put a colon directly after the
show keyword and then provide a function. This function is given the rest of the show keyword and then provide a function. This function is given the rest of the
document as a parameter. The function can then do anything with this content. document as a parameter. The function can then do anything with this content.
@ -52,7 +55,7 @@ just pass it by name to the show rule. Let's try it:
```example ```example
>>> #let amazed(term, color: blue) = { >>> #let amazed(term, color: blue) = {
>>> text(color, box[✨ #term ✨]) >>> text(color)[✨~#term~✨]
>>> } >>> }
#show: amazed #show: amazed
I choose to focus on the good I choose to focus on the good
@ -68,69 +71,56 @@ powerful.
## Embedding set and show rules { #set-and-show-rules } ## Embedding set and show rules { #set-and-show-rules }
To apply some set and show rules to our template, we can use `set` and `show` To apply some set and show rules to our template, we can use `set` and `show`
within a content block in our function and then insert the document into within a code block in our function and then insert the document into
that content block. that code block.
```example ```example
#let template(doc) = [ #let template(doc) = {
#set text(font: "Inria Serif") set text(font: "Inria Serif")
#show "something cool": [Typst] show "something cool": [Typst]
#doc doc
] }
#show: template #show: template
I am learning something cool today. I am learning something cool today.
It's going great so far! It's going great so far!
``` ```
Just like we already discovered in the previous chapter, set rules will apply to Just like we already discovered in the previous chapter, set rules will apply
everything within their content block. Since the everything show rule passes our to everything within their scope. Since the global show rule passes our whole
whole document to the `template` function, the text set rule and string show document to the `template` function, the text set rule and string show rule in
rule in our template will apply to the whole document. Let's use this knowledge our template will apply to the whole document.
to create a template that reproduces the body style of the paper we wrote in the
previous chapter. We used a curly-braced code block instead of a content block. This way, we
don't need to prefix all set rules and function calls with a `#`. This also
removes the implicit spaces that are naturally introduced in the markup mode.
In exchange, we cannot write markup directly in the code block anymore.
Let's use this knowledge to create a template that reproduces the body style of
the paper we wrote in the previous chapter.
```example ```example
#let conf(title, doc) = { #let conf(title, doc) = {
set page( set page(
paper: "us-letter", paper: "us-letter",
>>> margin: auto, header: align(right, title),
header: align(
right + horizon,
title
),
columns: 2, columns: 2,
<<< ... <<< ...
) )
set par(justify: true) set par(justify: true)
set text( set text(
11pt,
font: "Libertinus Serif", font: "Libertinus Serif",
size: 11pt,
) )
// Heading show rules. // Heading show rules.
<<< ... <<< ...
>>> show heading.where( >>> show heading.where(level: 1): set align(center)
>>> level: 1 >>> show heading.where(level: 1): set text(13pt, weight: "regular")
>>> ): it => block( >>> show heading.where(level: 1): it => block(smallcaps(it.body))
>>> align(center, >>>
>>> text( >>> show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
>>> 13pt, >>> show heading.where(level: 2): it => [#it.body.]
>>> weight: "regular",
>>> smallcaps(it.body),
>>> )
>>> ),
>>> )
>>> show heading.where(
>>> level: 2
>>> ): it => box(
>>> text(
>>> 11pt,
>>> weight: "regular",
>>> style: "italic",
>>> it.body + [.],
>>> )
>>> )
doc doc
} }
@ -154,24 +144,17 @@ previous chapter.
>>> #lorem(200) >>> #lorem(200)
``` ```
We copy-pasted most of that code from the previous chapter. The two differences We copied most of that code from the previous chapter. However, now we wrapped
are this: everything in the function `conf` using a global show rule. The function applies
a few set and show rules and echoes the content it has been passed at the end.
1. We wrapped everything in the function `conf` using an everything show rule. Also note where the title comes from: we previously had it inside of a variable.
The function applies a few set and show rules and echoes the content it has
been passed at the end.
2. Moreover, we used a curly-braced code block instead of a content block. This
way, we don't need to prefix all set rules and function calls with a `#`. In
exchange, we cannot write markup directly in the code block anymore.
Also note where the title comes from: We previously had it inside of a variable.
Now, we are receiving it as the first parameter of the template function. To do Now, we are receiving it as the first parameter of the template function. To do
so, we passed a closure (that's a function without a name that is used right so, we passed a closure (that's a function without a name that is used right
away) to the everything show rule. We did that because the `conf` function away) to the global show rule. We did that because the `conf` function expects
expects two positional arguments, the title and the body, but the show rule will two positional arguments: the title and the body, but the show rule will only
only pass the body. Therefore, we add a new function definition that allows us pass the body. Therefore, we add a new function definition that allows us to set
to set a paper title and use the single parameter from the show rule. a paper title and use the single parameter from the show rule.
## Templates with named arguments { #named-arguments } ## Templates with named arguments { #named-arguments }
Our paper in the previous chapter had a title and an author list. Let's add Our paper in the previous chapter had a title and an author list. Let's add
@ -230,6 +213,9 @@ multiple arguments for the grid. We can do that by using the
[`spread` operator]($arguments). It takes an array and applies each of its items [`spread` operator]($arguments). It takes an array and applies each of its items
as a separate argument to the function. as a separate argument to the function.
Let's also include some PDF metadata. We can achieve this by using
the [`document`] function and specifying fields such as `title` and `author`.
The resulting template function looks like this: The resulting template function looks like this:
```typ ```typ
@ -239,12 +225,15 @@ The resulting template function looks like this:
abstract: [], abstract: [],
doc, doc,
) = { ) = {
set document(title: title, author: authors.map(author => author.name))
// Set and show rules from before. // Set and show rules from before.
>>> #set page(columns: 2)
<<< ... <<< ...
{
set align(center) set align(center)
text(17pt, title) set par(justify: false)
block(text(17pt, strong(title)))
let count = authors.len() let count = authors.len()
let ncols = calc.min(count, 3) let ncols = calc.min(count, 3)
@ -258,12 +247,11 @@ The resulting template function looks like this:
]), ]),
) )
par(justify: false)[ strong[Abstract]
*Abstract* \ linebreak()
#abstract abstract
] }
set align(left)
doc doc
} }
``` ```
@ -291,49 +279,26 @@ call.
>>> abstract: [], >>> abstract: [],
>>> doc, >>> doc,
>>> ) = { >>> ) = {
>>> set text(font: "Libertinus Serif", 11pt) >>> set document(title: title, author: authors.map(author => author.name))
>>> set par(justify: true)
>>> set page( >>> set page(
>>> "us-letter", >>> "us-letter",
>>> margin: auto, >>> header: align(right, title),
>>> header: align(
>>> right + horizon,
>>> title
>>> ),
>>> numbering: "1", >>> numbering: "1",
>>> columns: 2, >>> columns: 2,
>>> ) >>> )
>>> set par(justify: true)
>>> set text(11pt, font: "Libertinus Serif")
>>> >>>
>>> show heading.where( >>> show heading.where(level: 1): set align(center)
>>> level: 1 >>> show heading.where(level: 1): set text(13pt, weight: "regular")
>>> ): it => block( >>> show heading.where(level: 1): it => block(smallcaps(it.body))
>>> align(center,
>>> text(
>>> 13pt,
>>> weight: "regular",
>>> smallcaps(it.body),
>>> )
>>> ),
>>> )
>>> show heading.where(
>>> level: 2
>>> ): it => box(
>>> text(
>>> 11pt,
>>> weight: "regular",
>>> style: "italic",
>>> it.body + [.],
>>> )
>>> )
>>> >>>
>>> place( >>> show heading.where(level: 2): set text(11pt, weight: "regular", style: "italic")
>>> top, >>> show heading.where(level: 2): it => [#it.body.]
>>> float: true, >>>
>>> scope: "parent", >>> place(top + center, float: true, scope: "parent", clearance: 2em, {
>>> clearance: 2em, >>> set par(justify: false)
>>> { >>> block(text(17pt, title))
>>> set align(center)
>>> text(17pt, title)
>>> let count = calc.min(authors.len(), 3) >>> let count = calc.min(authors.len(), 3)
>>> grid( >>> grid(
>>> columns: (1fr,) * count, >>> columns: (1fr,) * count,
@ -344,19 +309,15 @@ call.
>>> #link("mailto:" + author.email) >>> #link("mailto:" + author.email)
>>> ]), >>> ]),
>>> ) >>> )
>>> par(justify: false)[ >>> strong[Abstract]
>>> *Abstract* \ >>> linebreak()
>>> #abstract >>> abstract
>>> ] >>> })
>>> },
>>> )
>>> doc >>> doc
>>>} >>> }
<<< #import "conf.typ": conf <<< #import "conf.typ": conf
#show: conf.with( #show: conf.with(
title: [ title: [Towards Improved Modelling],
Towards Improved Modelling
],
authors: ( authors: (
( (
name: "Theresa Tungsten", name: "Theresa Tungsten",
@ -397,7 +358,7 @@ that define reusable document styles. You've made it far and learned a lot. You
can now use Typst to write your own documents and share them with others. can now use Typst to write your own documents and share them with others.
We are still a super young project and are looking for feedback. If you have any We are still a super young project and are looking for feedback. If you have any
questions, suggestions or you found a bug, please let us know questions, suggestions, or you found a bug, please let us know
in the [Forum](https://forum.typst.app/), in the [Forum](https://forum.typst.app/),
on our [Discord server](https://discord.gg/2uDybryKPe), on our [Discord server](https://discord.gg/2uDybryKPe),
on [GitHub](https://github.com/typst/typst/), on [GitHub](https://github.com/typst/typst/),

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

@ -258,7 +258,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
--- image-png-but-pixmap-format --- --- image-png-but-pixmap-format ---
#image( #image(
read("/assets/images/tiger.jpg", encoding: none), 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", 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(v => raw(str(v), lang: "typc")),
..rotations.map(rotated) ..rotations.map(rotated)
) )
--- image-pdf ---
#image("/assets/images/matplotlib.pdf")
--- 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)
--- image-pdf-invalid-page ---
// Error: 2-49 page 2 does not exist
// Hint: 2-49 the document only has 1 page
#image("/assets/images/matplotlib.pdf", page: 2)