mirror of
https://github.com/typst/typst
synced 2025-06-08 13:16:24 +08:00
more progress
This commit is contained in:
parent
c915f69e87
commit
15faecbf27
@ -68,7 +68,7 @@ if_chain = "1"
|
|||||||
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] }
|
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] }
|
||||||
indexmap = { version = "2", features = ["serde"] }
|
indexmap = { version = "2", features = ["serde"] }
|
||||||
kamadak-exif = "0.5"
|
kamadak-exif = "0.5"
|
||||||
krilla = { path = "../krilla/crates/krilla", features = ["comemo", "rayon"] }
|
krilla = { path = "../krilla/crates/krilla", features = ["comemo", "rayon", "svg"] }
|
||||||
kurbo = "0.11"
|
kurbo = "0.11"
|
||||||
libfuzzer-sys = "0.4"
|
libfuzzer-sys = "0.4"
|
||||||
lipsum = "0.9"
|
lipsum = "0.9"
|
||||||
|
@ -1,26 +1,31 @@
|
|||||||
|
use crate::AbsExt;
|
||||||
use crate::{AbsExt};
|
use bytemuck::TransparentWrapper;
|
||||||
|
use image::{DynamicImage, GenericImageView, Rgba};
|
||||||
|
use krilla::action::{Action, LinkAction};
|
||||||
|
use krilla::annotation::{LinkAnnotation, Target};
|
||||||
use krilla::color::rgb;
|
use krilla::color::rgb;
|
||||||
|
use krilla::destination::XyzDestination;
|
||||||
use krilla::font::{GlyphId, GlyphUnits};
|
use krilla::font::{GlyphId, GlyphUnits};
|
||||||
use krilla::geom::{Point, Transform};
|
use krilla::geom::{Point, Transform};
|
||||||
|
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
|
||||||
use krilla::path::{Fill, PathBuilder, Stroke};
|
use krilla::path::{Fill, PathBuilder, Stroke};
|
||||||
use krilla::surface::Surface;
|
use krilla::surface::Surface;
|
||||||
|
use krilla::validation::Validator;
|
||||||
|
use krilla::version::PdfVersion;
|
||||||
use krilla::{PageSettings, SerializeSettings, SvgSettings};
|
use krilla::{PageSettings, SerializeSettings, SvgSettings};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
use bytemuck::TransparentWrapper;
|
|
||||||
use image::{DynamicImage, GenericImageView, Rgba};
|
|
||||||
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
|
|
||||||
use krilla::validation::Validator;
|
|
||||||
use krilla::version::PdfVersion;
|
|
||||||
use svg2pdf::usvg::{NormalizedF32, Rect};
|
use svg2pdf::usvg::{NormalizedF32, Rect};
|
||||||
use svg2pdf::usvg::filter::ColorChannel;
|
use typst_library::layout::{Abs, Frame, FrameItem, GroupItem, Page, Size};
|
||||||
use typst_library::layout::{Frame, FrameItem, GroupItem, Size};
|
use typst_library::model::{Destination, Document};
|
||||||
use typst_library::model::Document;
|
|
||||||
use typst_library::text::{Font, Glyph, TextItem};
|
use typst_library::text::{Font, Glyph, TextItem};
|
||||||
use typst_library::visualize::{ColorSpace, FillRule, FixedStroke, Geometry, Image, ImageKind, LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RasterImage, Shape};
|
use typst_library::visualize::{
|
||||||
|
ColorSpace, FillRule, FixedStroke, Geometry, Image, ImageKind, LineCap, LineJoin,
|
||||||
|
Paint, Path, PathItem, RasterFormat, RasterImage, Shape,
|
||||||
|
};
|
||||||
|
use typst_syntax::ast::Link;
|
||||||
|
|
||||||
#[derive(TransparentWrapper)]
|
#[derive(TransparentWrapper)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
@ -54,12 +59,16 @@ impl krilla::font::Glyph for PdfGlyph {
|
|||||||
|
|
||||||
pub struct ExportContext {
|
pub struct ExportContext {
|
||||||
fonts: HashMap<Font, krilla::font::Font>,
|
fonts: HashMap<Font, krilla::font::Font>,
|
||||||
|
cur_transform: typst_library::layout::Transform,
|
||||||
|
annotations: Vec<krilla::annotation::Annotation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExportContext {
|
impl ExportContext {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
fonts: Default::default(),
|
fonts: Default::default(),
|
||||||
|
cur_transform: typst_library::layout::Transform::identity(),
|
||||||
|
annotations: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,6 +99,12 @@ pub fn pdf(typst_document: &Document) -> Vec<u8> {
|
|||||||
let mut page = document.start_page_with(settings);
|
let mut page = document.start_page_with(settings);
|
||||||
let mut surface = page.surface();
|
let mut surface = page.surface();
|
||||||
process_frame(&typst_page.frame, &mut surface, &mut context);
|
process_frame(&typst_page.frame, &mut surface, &mut context);
|
||||||
|
surface.finish();
|
||||||
|
|
||||||
|
let annotations = std::mem::take(&mut context.annotations);
|
||||||
|
for annotation in annotations {
|
||||||
|
page.add_annotation(annotation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finish(document)
|
finish(document)
|
||||||
@ -106,8 +121,13 @@ pub fn handle_group(
|
|||||||
surface: &mut Surface,
|
surface: &mut Surface,
|
||||||
context: &mut ExportContext,
|
context: &mut ExportContext,
|
||||||
) {
|
) {
|
||||||
|
let old = context.cur_transform;
|
||||||
|
context.cur_transform = context.cur_transform.pre_concat(group.transform);
|
||||||
|
|
||||||
surface.push_transform(&convert_transform(group.transform));
|
surface.push_transform(&convert_transform(group.transform));
|
||||||
process_frame(&group.frame, surface, context);
|
process_frame(&group.frame, surface, context);
|
||||||
|
|
||||||
|
context.cur_transform = old;
|
||||||
surface.pop();
|
surface.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,14 +136,9 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont
|
|||||||
.fonts
|
.fonts
|
||||||
.entry(t.font.clone())
|
.entry(t.font.clone())
|
||||||
.or_insert_with(|| {
|
.or_insert_with(|| {
|
||||||
krilla::font::Font::new(
|
krilla::font::Font::new(Arc::new(t.font.data().clone()), t.font.index(), true)
|
||||||
// TODO: Don't do to_vec here!
|
|
||||||
Arc::new(t.font.data().to_vec()),
|
|
||||||
t.font.index(),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
// TODO: DOn't unwrap
|
// TODO: DOn't unwrap
|
||||||
.unwrap()
|
.unwrap()
|
||||||
})
|
})
|
||||||
.clone();
|
.clone();
|
||||||
let (paint, opacity) = convert_paint(&t.fill);
|
let (paint, opacity) = convert_paint(&t.fill);
|
||||||
@ -166,7 +181,7 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont
|
|||||||
struct PdfImage {
|
struct PdfImage {
|
||||||
raster: RasterImage,
|
raster: RasterImage,
|
||||||
alpha_channel: OnceLock<Option<Arc<Vec<u8>>>>,
|
alpha_channel: OnceLock<Option<Arc<Vec<u8>>>>,
|
||||||
actual_dynamic: OnceLock<Arc<DynamicImage>>
|
actual_dynamic: OnceLock<Arc<DynamicImage>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PdfImage {
|
impl PdfImage {
|
||||||
@ -187,29 +202,36 @@ impl Hash for PdfImage {
|
|||||||
|
|
||||||
impl CustomImage for PdfImage {
|
impl CustomImage for PdfImage {
|
||||||
fn color_channel(&self) -> &[u8] {
|
fn color_channel(&self) -> &[u8] {
|
||||||
self.actual_dynamic.get_or_init(|| {
|
self.actual_dynamic
|
||||||
let dynamic = self.raster.dynamic();
|
.get_or_init(|| {
|
||||||
let channel_count = dynamic.color().channel_count();
|
let dynamic = self.raster.dynamic();
|
||||||
|
let channel_count = dynamic.color().channel_count();
|
||||||
|
|
||||||
match (dynamic.as_ref(), channel_count) {
|
match (dynamic.as_ref(), channel_count) {
|
||||||
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
|
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
|
||||||
(DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
|
(DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
|
||||||
(_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
|
(_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
|
||||||
_ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
|
_ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
|
||||||
}
|
}
|
||||||
}).as_bytes()
|
})
|
||||||
|
.as_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn alpha_channel(&self) -> Option<&[u8]> {
|
fn alpha_channel(&self) -> Option<&[u8]> {
|
||||||
self.alpha_channel.get_or_init(||
|
self.alpha_channel
|
||||||
self.raster.dynamic().color().has_alpha()
|
.get_or_init(|| {
|
||||||
.then(|| Arc::new(self.raster
|
self.raster.dynamic().color().has_alpha().then(|| {
|
||||||
.dynamic()
|
Arc::new(
|
||||||
.pixels()
|
self.raster
|
||||||
.map(|(_, _, Rgba([_, _, _, a]))| a)
|
.dynamic()
|
||||||
.collect())
|
.pixels()
|
||||||
)
|
.map(|(_, _, Rgba([_, _, _, a]))| a)
|
||||||
).as_ref().map(|v| &***v)
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| &***v)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bits_per_component(&self) -> BitsPerComponent {
|
fn bits_per_component(&self) -> BitsPerComponent {
|
||||||
@ -220,10 +242,26 @@ impl CustomImage for PdfImage {
|
|||||||
(self.raster.width(), self.raster.height())
|
(self.raster.width(), self.raster.height())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn icc_profile(&self) -> Option<&[u8]> {
|
||||||
|
if matches!(
|
||||||
|
self.raster.dynamic().as_ref(),
|
||||||
|
DynamicImage::ImageLuma8(_)
|
||||||
|
| DynamicImage::ImageLumaA8(_)
|
||||||
|
| DynamicImage::ImageRgb8(_)
|
||||||
|
| DynamicImage::ImageRgba8(_)
|
||||||
|
) {
|
||||||
|
self.raster.icc()
|
||||||
|
} else {
|
||||||
|
// In all other cases, the dynamic will be converted into RGB8, so the ICC
|
||||||
|
// profile may become invalid, and thus we don't include it.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn color_space(&self) -> ImageColorspace {
|
fn color_space(&self) -> ImageColorspace {
|
||||||
if self.raster.dynamic().color().has_color() {
|
if self.raster.dynamic().color().has_color() {
|
||||||
ImageColorspace::Rgb
|
ImageColorspace::Rgb
|
||||||
} else {
|
} else {
|
||||||
ImageColorspace::Luma
|
ImageColorspace::Luma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,10 +295,12 @@ pub fn handle_image(
|
|||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn convert_raster(raster: RasterImage) -> krilla::image::Image {
|
fn convert_raster(raster: RasterImage) -> krilla::image::Image {
|
||||||
match raster.format() {
|
match raster.format() {
|
||||||
// TODO: Remove to_vec
|
RasterFormat::Jpg => {
|
||||||
RasterFormat::Jpg => krilla::image::Image::from_jpeg(Arc::new(raster.data().to_vec())),
|
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
|
||||||
_ => krilla::image::Image::from_custom(PdfImage::new(raster))
|
}
|
||||||
}.unwrap()
|
_ => krilla::image::Image::from_custom(PdfImage::new(raster)),
|
||||||
|
}
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_shape(shape: &Shape, surface: &mut Surface) {
|
pub fn handle_shape(shape: &Shape, surface: &mut Surface) {
|
||||||
@ -333,7 +373,7 @@ pub fn process_frame(frame: &Frame, surface: &mut Surface, context: &mut ExportC
|
|||||||
FrameItem::Image(image, size, _) => {
|
FrameItem::Image(image, size, _) => {
|
||||||
handle_image(image, size, surface, context)
|
handle_image(image, size, surface, context)
|
||||||
}
|
}
|
||||||
FrameItem::Link(_, _) => {}
|
FrameItem::Link(d, s) => handle_link(*point, d, *s, context, surface),
|
||||||
FrameItem::Tag(_) => {}
|
FrameItem::Tag(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,6 +381,53 @@ pub fn process_frame(frame: &Frame, surface: &mut Surface, context: &mut ExportC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_link(
|
||||||
|
pos: typst_library::layout::Point,
|
||||||
|
dest: &Destination,
|
||||||
|
size: typst_library::layout::Size,
|
||||||
|
ctx: &mut ExportContext,
|
||||||
|
surface: &mut Surface,
|
||||||
|
) {
|
||||||
|
let mut min_x = Abs::inf();
|
||||||
|
let mut min_y = Abs::inf();
|
||||||
|
let mut max_x = -Abs::inf();
|
||||||
|
let mut max_y = -Abs::inf();
|
||||||
|
|
||||||
|
// Compute the bounding box of the transformed link.
|
||||||
|
for point in [
|
||||||
|
pos,
|
||||||
|
pos + typst_library::layout::Point::with_x(size.x),
|
||||||
|
pos + typst_library::layout::Point::with_y(size.y),
|
||||||
|
pos + size.to_point(),
|
||||||
|
] {
|
||||||
|
let t = point.transform(ctx.cur_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 = krilla::geom::Rect::from_ltrb(x1, y1, x2, y2).unwrap();
|
||||||
|
|
||||||
|
let target = match dest {
|
||||||
|
Destination::Url(u) => {
|
||||||
|
Target::Action(Action::Link(LinkAction::new(u.to_string())))
|
||||||
|
}
|
||||||
|
Destination::Position(p) => {
|
||||||
|
Target::Destination(krilla::destination::Destination::Xyz(
|
||||||
|
XyzDestination::new(p.page.get() - 1, convert_point(p.point)),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Destination::Location(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.annotations.push(LinkAnnotation::new(rect, target).into());
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_fill_rule(fill_rule: FillRule) -> krilla::path::FillRule {
|
fn convert_fill_rule(fill_rule: FillRule) -> krilla::path::FillRule {
|
||||||
match fill_rule {
|
match fill_rule {
|
||||||
FillRule::NonZero => krilla::path::FillRule::NonZero,
|
FillRule::NonZero => krilla::path::FillRule::NonZero,
|
||||||
@ -361,6 +448,10 @@ fn convert_fixed_stroke(stroke: &FixedStroke) -> Stroke {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_point(p: typst_library::layout::Point) -> krilla::geom::Point {
|
||||||
|
Point::from_xy(p.x.to_f32(), p.y.to_f32())
|
||||||
|
}
|
||||||
|
|
||||||
fn convert_linecap(l: LineCap) -> krilla::path::LineCap {
|
fn convert_linecap(l: LineCap) -> krilla::path::LineCap {
|
||||||
match l {
|
match l {
|
||||||
LineCap::Butt => krilla::path::LineCap::Butt,
|
LineCap::Butt => krilla::path::LineCap::Butt,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user