mirror of
https://github.com/typst/typst
synced 2025-06-08 05:06:24 +08:00
more
This commit is contained in:
parent
71e73f9053
commit
3e32e4c373
@ -1,8 +1,57 @@
|
|||||||
use image::{DynamicImage, GenericImageView, Rgba};
|
|
||||||
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
|
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use typst_library::visualize::{RasterFormat, RasterImage};
|
|
||||||
|
use image::{DynamicImage, GenericImageView, Rgba};
|
||||||
|
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
|
||||||
|
use krilla::surface::Surface;
|
||||||
|
use krilla::SvgSettings;
|
||||||
|
use typst_library::diag::{bail, SourceResult};
|
||||||
|
use typst_library::layout::Size;
|
||||||
|
use typst_library::visualize::{Image, ImageKind, RasterFormat, RasterImage};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
|
use crate::krilla::{FrameContext, GlobalContext};
|
||||||
|
use crate::util::{SizeExt, TransformExt};
|
||||||
|
|
||||||
|
pub(crate) fn handle_image(
|
||||||
|
gc: &mut GlobalContext,
|
||||||
|
fc: &mut FrameContext,
|
||||||
|
image: &Image,
|
||||||
|
size: Size,
|
||||||
|
surface: &mut Surface,
|
||||||
|
span: Span,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
surface.push_transform(&fc.state().transform.to_krilla());
|
||||||
|
|
||||||
|
match image.kind() {
|
||||||
|
ImageKind::Raster(raster) => {
|
||||||
|
let image = match convert_raster(raster.clone()) {
|
||||||
|
None => bail!(span, "failed to process image"),
|
||||||
|
Some(i) => i,
|
||||||
|
};
|
||||||
|
|
||||||
|
if gc.image_spans.contains_key(&image) {
|
||||||
|
gc.image_spans.insert(image.clone(), span);
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.draw_image(image, size.to_krilla());
|
||||||
|
}
|
||||||
|
ImageKind::Svg(svg) => {
|
||||||
|
surface.draw_svg(
|
||||||
|
svg.tree(),
|
||||||
|
size.to_krilla(),
|
||||||
|
SvgSettings {
|
||||||
|
embed_text: !svg.flatten_text(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.pop();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// A wrapper around RasterImage so that we can implement `CustomImage`.
|
/// A wrapper around RasterImage so that we can implement `CustomImage`.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -106,13 +155,13 @@ impl CustomImage for PdfImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
pub(crate) fn raster(raster: RasterImage) -> Option<krilla::image::Image> {
|
fn convert_raster(raster: RasterImage) -> Option<krilla::image::Image> {
|
||||||
match raster.format() {
|
match raster.format() {
|
||||||
RasterFormat::Jpg => {
|
RasterFormat::Jpg => {
|
||||||
if !raster.is_rotated() {
|
if !raster.is_rotated() {
|
||||||
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
|
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
|
||||||
} else {
|
} else {
|
||||||
// Can't embed original JPEG data if it needed to be rotated.
|
// Can't embed original JPEG data if it had to be rotated.
|
||||||
krilla::image::Image::from_custom(PdfImage::new(raster))
|
krilla::image::Image::from_custom(PdfImage::new(raster))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
use crate::image::handle_image;
|
||||||
|
use crate::link::handle_link;
|
||||||
use crate::metadata::build_metadata;
|
use crate::metadata::build_metadata;
|
||||||
use crate::outline::build_outline;
|
use crate::outline::build_outline;
|
||||||
use crate::page::PageLabelExt;
|
use crate::page::PageLabelExt;
|
||||||
use crate::util::{display_font, AbsExt, PointExt, SizeExt, TransformExt};
|
use crate::util::{build_path, display_font, AbsExt, PointExt, SizeExt, TransformExt};
|
||||||
use crate::{paint, PdfOptions};
|
use crate::{paint, PdfOptions};
|
||||||
use bytemuck::TransparentWrapper;
|
use bytemuck::TransparentWrapper;
|
||||||
use krilla::action::{Action, LinkAction};
|
use krilla::action::{Action, LinkAction};
|
||||||
@ -37,7 +39,7 @@ pub(crate) struct State {
|
|||||||
/// The full transform chain
|
/// The full transform chain
|
||||||
transform_chain: Transform,
|
transform_chain: Transform,
|
||||||
/// The transform of the current item.
|
/// The transform of the current item.
|
||||||
transform: Transform,
|
pub(crate) transform: Transform,
|
||||||
/// The transform of first hard frame in the hierarchy.
|
/// The transform of first hard frame in the hierarchy.
|
||||||
container_transform_chain: Transform,
|
container_transform_chain: Transform,
|
||||||
/// The size of the first hard frame in the hierarchy.
|
/// The size of the first hard frame in the hierarchy.
|
||||||
@ -85,7 +87,7 @@ impl State {
|
|||||||
|
|
||||||
pub(crate) struct FrameContext {
|
pub(crate) struct FrameContext {
|
||||||
states: Vec<State>,
|
states: Vec<State>,
|
||||||
annotations: Vec<krilla::annotation::Annotation>,
|
pub(crate) annotations: Vec<krilla::annotation::Annotation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameContext {
|
impl FrameContext {
|
||||||
@ -164,11 +166,11 @@ pub struct GlobalContext<'a> {
|
|||||||
// if it appears in the document multiple times. We just store the
|
// if it appears in the document multiple times. We just store the
|
||||||
// first appearance, though.
|
// first appearance, though.
|
||||||
/// Mapping between images and their span.
|
/// Mapping between images and their span.
|
||||||
image_spans: HashMap<krilla::image::Image, Span>,
|
pub(crate) image_spans: HashMap<krilla::image::Image, Span>,
|
||||||
pub(crate) document: &'a PagedDocument,
|
pub(crate) document: &'a PagedDocument,
|
||||||
pub(crate) options: &'a PdfOptions<'a>,
|
pub(crate) options: &'a PdfOptions<'a>,
|
||||||
/// Mapping between locations in the document and named destinations.
|
/// Mapping between locations in the document and named destinations.
|
||||||
loc_to_named: HashMap<Location, NamedDestination>,
|
pub(crate) loc_to_named: HashMap<Location, NamedDestination>,
|
||||||
/// The languages used throughout the document.
|
/// The languages used throughout the document.
|
||||||
pub(crate) languages: BTreeMap<Lang, usize>,
|
pub(crate) languages: BTreeMap<Lang, usize>,
|
||||||
}
|
}
|
||||||
@ -200,25 +202,6 @@ impl<'a> GlobalContext<'a> {
|
|||||||
|
|
||||||
// TODO: Change rustybuzz cluster behavior so it works with ActualText
|
// TODO: Change rustybuzz cluster behavior so it works with ActualText
|
||||||
|
|
||||||
fn get_version(options: &PdfOptions) -> SourceResult<PdfVersion> {
|
|
||||||
match options.pdf_version {
|
|
||||||
None => Ok(options.validator.recommended_version()),
|
|
||||||
Some(v) => {
|
|
||||||
if !options.validator.compatible_with_version(v) {
|
|
||||||
let v_string = v.as_str();
|
|
||||||
let s_string = options.validator.as_str();
|
|
||||||
let h_message = format!(
|
|
||||||
"export using {} instead",
|
|
||||||
options.validator.recommended_version().as_str()
|
|
||||||
);
|
|
||||||
bail!(Span::detached(), "{v_string} is not compatible with standard {s_string}"; hint: "{h_message}");
|
|
||||||
} else {
|
|
||||||
Ok(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[typst_macros::time(name = "write pdf")]
|
#[typst_macros::time(name = "write pdf")]
|
||||||
pub fn pdf(
|
pub fn pdf(
|
||||||
typst_document: &PagedDocument,
|
typst_document: &PagedDocument,
|
||||||
@ -278,7 +261,7 @@ pub fn pdf(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut document = krilla::Document::new_with(settings);
|
let mut document = Document::new_with(settings);
|
||||||
let mut gc = GlobalContext::new(&typst_document, options, locs_to_names);
|
let mut gc = GlobalContext::new(&typst_document, options, locs_to_names);
|
||||||
|
|
||||||
let mut skipped_pages = 0;
|
let mut skipped_pages = 0;
|
||||||
@ -340,6 +323,7 @@ pub fn pdf(
|
|||||||
finish(document, gc)
|
finish(document, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Finish a krilla document and handle export errors.
|
||||||
fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
||||||
match document.finish() {
|
match document.finish() {
|
||||||
Ok(r) => Ok(r),
|
Ok(r) => Ok(r),
|
||||||
@ -360,7 +344,8 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
|||||||
);
|
);
|
||||||
match &ve[0] {
|
match &ve[0] {
|
||||||
ValidationError::TooLongString => {
|
ValidationError::TooLongString => {
|
||||||
bail!(Span::detached(), "{prefix} a PDF string longer than 32767 characters";
|
bail!(Span::detached(), "{prefix} a PDF string longer \
|
||||||
|
than 32767 characters";
|
||||||
hint: "make sure title and author names are short enough");
|
hint: "make sure title and author names are short enough");
|
||||||
}
|
}
|
||||||
// Should in theory never occur, as krilla always trims font names
|
// Should in theory never occur, as krilla always trims font names
|
||||||
@ -373,11 +358,13 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
|||||||
hint: "this can happen if you have a very long text in a single line");
|
hint: "this can happen if you have a very long text in a single line");
|
||||||
}
|
}
|
||||||
ValidationError::TooLongDictionary => {
|
ValidationError::TooLongDictionary => {
|
||||||
bail!(Span::detached(), "{prefix} a PDF dictionary had more than 4095 entries";
|
bail!(Span::detached(), "{prefix} a PDF dictionary had \
|
||||||
|
more than 4095 entries";
|
||||||
hint: "try reducing the complexity of your document");
|
hint: "try reducing the complexity of your document");
|
||||||
}
|
}
|
||||||
ValidationError::TooLargeFloat => {
|
ValidationError::TooLargeFloat => {
|
||||||
bail!(Span::detached(), "{prefix} a PDF float was larger than the allowed limit";
|
bail!(Span::detached(), "{prefix} a PDF float was larger than \
|
||||||
|
the allowed limit";
|
||||||
hint: "try exporting using a higher PDF version");
|
hint: "try exporting using a higher PDF version");
|
||||||
}
|
}
|
||||||
ValidationError::TooManyIndirectObjects => {
|
ValidationError::TooManyIndirectObjects => {
|
||||||
@ -402,17 +389,23 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
|||||||
hint: "ensure all text can be displayed using an available font");
|
hint: "ensure all text can be displayed using an available font");
|
||||||
}
|
}
|
||||||
ValidationError::InvalidCodepointMapping(_, _) => {
|
ValidationError::InvalidCodepointMapping(_, _) => {
|
||||||
bail!(Span::detached(), "{prefix} the PDF contains the disallowed codepoints";
|
bail!(Span::detached(), "{prefix} the PDF contains the \
|
||||||
hint: "make sure to not use the Unicode characters 0x0, 0xFEFF or 0xFFFE");
|
disallowed codepoints";
|
||||||
|
hint: "make sure to not use the Unicode characters 0x0, \
|
||||||
|
0xFEFF or 0xFFFE");
|
||||||
}
|
}
|
||||||
ValidationError::UnicodePrivateArea(_, _) => {
|
ValidationError::UnicodePrivateArea(_, _) => {
|
||||||
bail!(Span::detached(), "{prefix} the PDF contains characters from the Unicode private area";
|
bail!(Span::detached(), "{prefix} the PDF contains characters from the \
|
||||||
hint: "remove the text containing codepoints from the Unicode private area");
|
Unicode private area";
|
||||||
|
hint: "remove the text containing codepoints \
|
||||||
|
from the Unicode private area");
|
||||||
}
|
}
|
||||||
ValidationError::Transparency => {
|
ValidationError::Transparency => {
|
||||||
bail!(Span::detached(), "{prefix} document contains transparency";
|
bail!(Span::detached(), "{prefix} document contains transparency";
|
||||||
hint: "remove any transparency from your document (e.g. fills with opacity)";
|
hint: "remove any transparency from your \
|
||||||
hint: "you might have to convert certain SVGs into a bitmap image if they contain transparency";
|
document (e.g. fills with opacity)";
|
||||||
|
hint: "you might have to convert certain SVGs into a bitmap image if \
|
||||||
|
they contain transparency";
|
||||||
hint: "export using a different standard that supports transparency"
|
hint: "export using a different standard that supports transparency"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -456,6 +449,25 @@ fn finish(document: Document, gc: GlobalContext) -> SourceResult<Vec<u8>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_version(options: &PdfOptions) -> SourceResult<PdfVersion> {
|
||||||
|
match options.pdf_version {
|
||||||
|
None => Ok(options.validator.recommended_version()),
|
||||||
|
Some(v) => {
|
||||||
|
if !options.validator.compatible_with_version(v) {
|
||||||
|
let v_string = v.as_str();
|
||||||
|
let s_string = options.validator.as_str();
|
||||||
|
let h_message = format!(
|
||||||
|
"export using {} instead",
|
||||||
|
options.validator.recommended_version().as_str()
|
||||||
|
);
|
||||||
|
bail!(Span::detached(), "{v_string} is not compatible with standard {s_string}"; hint: "{h_message}");
|
||||||
|
} else {
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_frame(
|
pub fn process_frame(
|
||||||
fc: &mut FrameContext,
|
fc: &mut FrameContext,
|
||||||
frame: &Frame,
|
frame: &Frame,
|
||||||
@ -478,6 +490,7 @@ pub fn process_frame(
|
|||||||
for (point, item) in frame.items() {
|
for (point, item) in frame.items() {
|
||||||
fc.push();
|
fc.push();
|
||||||
fc.state_mut().transform(Transform::translate(point.x, point.y));
|
fc.state_mut().transform(Transform::translate(point.x, point.y));
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
FrameItem::Group(g) => handle_group(fc, g, surface, gc)?,
|
FrameItem::Group(g) => handle_group(fc, g, surface, gc)?,
|
||||||
FrameItem::Text(t) => handle_text(fc, t, surface, gc)?,
|
FrameItem::Text(t) => handle_text(fc, t, surface, gc)?,
|
||||||
@ -485,7 +498,7 @@ pub fn process_frame(
|
|||||||
FrameItem::Image(image, size, span) => {
|
FrameItem::Image(image, size, span) => {
|
||||||
handle_image(gc, fc, image, *size, surface, *span)?
|
handle_image(gc, fc, image, *size, surface, *span)?
|
||||||
}
|
}
|
||||||
FrameItem::Link(d, s) => write_link(fc, gc, d, *s),
|
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s),
|
||||||
FrameItem::Tag(_) => {}
|
FrameItem::Tag(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -497,85 +510,6 @@ pub fn process_frame(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a link for later writing in the annotations dictionary.
|
|
||||||
fn write_link(
|
|
||||||
fc: &mut FrameContext,
|
|
||||||
gc: &mut GlobalContext,
|
|
||||||
dest: &Destination,
|
|
||||||
size: Size,
|
|
||||||
) {
|
|
||||||
let mut min_x = Abs::inf();
|
|
||||||
let mut min_y = Abs::inf();
|
|
||||||
let mut max_x = -Abs::inf();
|
|
||||||
let mut max_y = -Abs::inf();
|
|
||||||
|
|
||||||
let pos = Point::zero();
|
|
||||||
|
|
||||||
// Compute the bounding box of the transformed link.
|
|
||||||
for point in [
|
|
||||||
pos,
|
|
||||||
pos + Point::with_x(size.x),
|
|
||||||
pos + Point::with_y(size.y),
|
|
||||||
pos + size.to_point(),
|
|
||||||
] {
|
|
||||||
let t = point.transform(fc.state().transform);
|
|
||||||
min_x.set_min(t.x);
|
|
||||||
min_y.set_min(t.y);
|
|
||||||
max_x.set_max(t.x);
|
|
||||||
max_y.set_max(t.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
let x1 = min_x.to_f32();
|
|
||||||
let x2 = max_x.to_f32();
|
|
||||||
let y1 = min_y.to_f32();
|
|
||||||
let y2 = max_y.to_f32();
|
|
||||||
|
|
||||||
let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap();
|
|
||||||
|
|
||||||
let pos = match dest {
|
|
||||||
Destination::Url(u) => {
|
|
||||||
fc.annotations.push(
|
|
||||||
LinkAnnotation::new(
|
|
||||||
rect,
|
|
||||||
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Destination::Position(p) => *p,
|
|
||||||
Destination::Location(loc) => {
|
|
||||||
if let Some(named_dest) = gc.loc_to_named.get(loc) {
|
|
||||||
fc.annotations.push(
|
|
||||||
LinkAnnotation::new(
|
|
||||||
rect,
|
|
||||||
Target::Destination(krilla::destination::Destination::Named(
|
|
||||||
named_dest.clone(),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
gc.document.introspector.position(*loc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let page_index = pos.page.get() - 1;
|
|
||||||
if !gc.page_excluded(page_index) {
|
|
||||||
fc.annotations.push(
|
|
||||||
LinkAnnotation::new(
|
|
||||||
rect,
|
|
||||||
Target::Destination(krilla::destination::Destination::Xyz(
|
|
||||||
XyzDestination::new(page_index, pos.point.to_krilla()),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_group(
|
pub fn handle_group(
|
||||||
fc: &mut FrameContext,
|
fc: &mut FrameContext,
|
||||||
group: &GroupItem,
|
group: &GroupItem,
|
||||||
@ -590,7 +524,7 @@ pub fn handle_group(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|p| {
|
.and_then(|p| {
|
||||||
let mut builder = PathBuilder::new();
|
let mut builder = PathBuilder::new();
|
||||||
convert_path(p, &mut builder);
|
build_path(p, &mut builder);
|
||||||
builder.finish()
|
builder.finish()
|
||||||
})
|
})
|
||||||
.and_then(|p| p.transform(fc.state().transform.to_krilla()));
|
.and_then(|p| p.transform(fc.state().transform.to_krilla()));
|
||||||
@ -691,46 +625,6 @@ pub fn handle_text(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_image(
|
|
||||||
gc: &mut GlobalContext,
|
|
||||||
fc: &mut FrameContext,
|
|
||||||
image: &Image,
|
|
||||||
size: Size,
|
|
||||||
surface: &mut Surface,
|
|
||||||
span: Span,
|
|
||||||
) -> SourceResult<()> {
|
|
||||||
surface.push_transform(&fc.state().transform.to_krilla());
|
|
||||||
|
|
||||||
match image.kind() {
|
|
||||||
ImageKind::Raster(raster) => {
|
|
||||||
let image = match crate::image::raster(raster.clone()) {
|
|
||||||
None => bail!(span, "failed to process image"),
|
|
||||||
Some(i) => i,
|
|
||||||
};
|
|
||||||
|
|
||||||
if gc.image_spans.contains_key(&image) {
|
|
||||||
gc.image_spans.insert(image.clone(), span);
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.draw_image(image, size.to_krilla());
|
|
||||||
}
|
|
||||||
ImageKind::Svg(svg) => {
|
|
||||||
surface.draw_svg(
|
|
||||||
svg.tree(),
|
|
||||||
size.to_krilla(),
|
|
||||||
SvgSettings {
|
|
||||||
embed_text: !svg.flatten_text(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.pop();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_shape(
|
pub fn handle_shape(
|
||||||
fc: &mut FrameContext,
|
fc: &mut FrameContext,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
@ -765,7 +659,7 @@ pub fn handle_shape(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Geometry::Path(p) => {
|
Geometry::Path(p) => {
|
||||||
convert_path(p, &mut path_builder);
|
build_path(p, &mut path_builder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -808,21 +702,3 @@ pub fn handle_shape(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn convert_path(path: &Path, builder: &mut PathBuilder) {
|
|
||||||
for item in &path.0 {
|
|
||||||
match item {
|
|
||||||
PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
|
|
||||||
PathItem::LineTo(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()),
|
|
||||||
PathItem::CubicTo(p1, p2, p3) => builder.cubic_to(
|
|
||||||
p1.x.to_f32(),
|
|
||||||
p1.y.to_f32(),
|
|
||||||
p2.x.to_f32(),
|
|
||||||
p2.y.to_f32(),
|
|
||||||
p3.x.to_f32(),
|
|
||||||
p3.y.to_f32(),
|
|
||||||
),
|
|
||||||
PathItem::ClosePath => builder.close(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
mod image;
|
mod image;
|
||||||
mod krilla;
|
mod krilla;
|
||||||
|
mod link;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod outline;
|
mod outline;
|
||||||
mod page;
|
mod page;
|
||||||
|
87
crates/typst-pdf/src/link.rs
Normal file
87
crates/typst-pdf/src/link.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
use crate::krilla::{FrameContext, GlobalContext};
|
||||||
|
use crate::util::{AbsExt, PointExt};
|
||||||
|
use krilla::action::{Action, LinkAction};
|
||||||
|
use krilla::annotation::{LinkAnnotation, Target};
|
||||||
|
use krilla::destination::XyzDestination;
|
||||||
|
use krilla::geom::Rect;
|
||||||
|
use typst_library::layout::{Abs, Point, Size};
|
||||||
|
use typst_library::model::Destination;
|
||||||
|
|
||||||
|
/// Save a link for later writing in the annotations dictionary.
|
||||||
|
pub(crate) fn handle_link(
|
||||||
|
fc: &mut FrameContext,
|
||||||
|
gc: &mut GlobalContext,
|
||||||
|
dest: &Destination,
|
||||||
|
size: Size,
|
||||||
|
) {
|
||||||
|
let mut min_x = Abs::inf();
|
||||||
|
let mut min_y = Abs::inf();
|
||||||
|
let mut max_x = -Abs::inf();
|
||||||
|
let mut max_y = -Abs::inf();
|
||||||
|
|
||||||
|
let pos = Point::zero();
|
||||||
|
|
||||||
|
// Compute the bounding box of the transformed link.
|
||||||
|
for point in [
|
||||||
|
pos,
|
||||||
|
pos + Point::with_x(size.x),
|
||||||
|
pos + Point::with_y(size.y),
|
||||||
|
pos + size.to_point(),
|
||||||
|
] {
|
||||||
|
let t = point.transform(fc.state().transform);
|
||||||
|
min_x.set_min(t.x);
|
||||||
|
min_y.set_min(t.y);
|
||||||
|
max_x.set_max(t.x);
|
||||||
|
max_y.set_max(t.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x1 = min_x.to_f32();
|
||||||
|
let x2 = max_x.to_f32();
|
||||||
|
let y1 = min_y.to_f32();
|
||||||
|
let y2 = max_y.to_f32();
|
||||||
|
|
||||||
|
let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap();
|
||||||
|
|
||||||
|
let pos = match dest {
|
||||||
|
Destination::Url(u) => {
|
||||||
|
fc.annotations.push(
|
||||||
|
LinkAnnotation::new(
|
||||||
|
rect,
|
||||||
|
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Destination::Position(p) => *p,
|
||||||
|
Destination::Location(loc) => {
|
||||||
|
if let Some(named_dest) = gc.loc_to_named.get(loc) {
|
||||||
|
fc.annotations.push(
|
||||||
|
LinkAnnotation::new(
|
||||||
|
rect,
|
||||||
|
Target::Destination(krilla::destination::Destination::Named(
|
||||||
|
named_dest.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
gc.document.introspector.position(*loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let page_index = pos.page.get() - 1;
|
||||||
|
if !gc.page_excluded(page_index) {
|
||||||
|
fc.annotations.push(
|
||||||
|
LinkAnnotation::new(
|
||||||
|
rect,
|
||||||
|
Target::Destination(krilla::destination::Destination::Xyz(
|
||||||
|
XyzDestination::new(page_index, pos.point.to_krilla()),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,12 @@
|
|||||||
use krilla::color::rgb as kr;
|
use krilla::color::rgb as kr;
|
||||||
use krilla::geom as kg;
|
use krilla::geom as kg;
|
||||||
use krilla::path as kp;
|
use krilla::path as kp;
|
||||||
|
use krilla::path::PathBuilder;
|
||||||
use typst_library::layout::{Abs, Point, Size, Transform};
|
use typst_library::layout::{Abs, Point, Size, Transform};
|
||||||
use typst_library::text::Font;
|
use typst_library::text::Font;
|
||||||
use typst_library::visualize::{Color, ColorSpace, FillRule, LineCap, LineJoin};
|
use typst_library::visualize::{
|
||||||
|
Color, ColorSpace, FillRule, LineCap, LineJoin, Path, PathItem,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) trait SizeExt {
|
pub(crate) trait SizeExt {
|
||||||
fn to_krilla(&self) -> kg::Size;
|
fn to_krilla(&self) -> kg::Size;
|
||||||
@ -117,3 +119,22 @@ pub(crate) fn display_font(font: &Font) -> String {
|
|||||||
let font_variant = font.info().variant;
|
let font_variant = font.info().variant;
|
||||||
format!("{} ({:?})", font_family, font_variant)
|
format!("{} ({:?})", font_family, font_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a typst path using a path builder.
|
||||||
|
pub(crate) fn build_path(path: &Path, builder: &mut PathBuilder) {
|
||||||
|
for item in &path.0 {
|
||||||
|
match item {
|
||||||
|
PathItem::MoveTo(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()),
|
||||||
|
PathItem::LineTo(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()),
|
||||||
|
PathItem::CubicTo(p1, p2, p3) => builder.cubic_to(
|
||||||
|
p1.x.to_f32(),
|
||||||
|
p1.y.to_f32(),
|
||||||
|
p2.x.to_f32(),
|
||||||
|
p2.y.to_f32(),
|
||||||
|
p3.x.to_f32(),
|
||||||
|
p3.y.to_f32(),
|
||||||
|
),
|
||||||
|
PathItem::ClosePath => builder.close(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user