This commit is contained in:
Laurenz Stampfl 2024-12-02 08:54:05 +01:00
parent 15faecbf27
commit 849994c827
8 changed files with 464 additions and 434 deletions

View File

@ -28,7 +28,7 @@ use typst_utils::{Deferred, Numeric, SliceExt};
use crate::color::PaintEncode; use crate::color::PaintEncode;
use crate::color_font::ColorFontMap; use crate::color_font::ColorFontMap;
use crate::extg::ExtGState; use crate::extg::ExtGState;
use crate::image::deferred_image; use crate::image_old::deferred_image;
use crate::resources::Resources; use crate::resources::Resources;
use crate::{deflate_deferred, AbsExt, ContentExt, EmExt, PdfOptions, StrExt}; use crate::{deflate_deferred, AbsExt, ContentExt, EmExt, PdfOptions, StrExt};

View File

@ -1,240 +1,116 @@
use std::collections::HashMap;
use std::io::Cursor;
use ecow::eco_format;
use image::{DynamicImage, GenericImageView, Rgba}; use image::{DynamicImage, GenericImageView, Rgba};
use pdf_writer::{Chunk, Filter, Finish, Ref}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use typst_library::diag::{At, SourceResult, StrResult}; use std::hash::{Hash, Hasher};
use typst_library::visualize::{ use std::sync::{Arc, OnceLock};
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, use typst_library::visualize::{RasterFormat, RasterImage};
};
use typst_utils::Deferred;
use crate::{color, deflate, PdfChunk, WithGlobalRefs}; /// A wrapper around RasterImage so that we can implement `CustomImage`.
#[derive(Clone)]
/// Embed all used images into the PDF. struct PdfImage {
#[typst_macros::time(name = "write images")] /// The original, underlying raster image.
pub fn write_images( raster: RasterImage,
context: &WithGlobalRefs, /// The alpha channel of the raster image, if existing.
) -> SourceResult<(PdfChunk, HashMap<Image, Ref>)> { alpha_channel: OnceLock<Option<Arc<Vec<u8>>>>,
let mut chunk = PdfChunk::new(); /// A (potentially) converted version of the dynamic image stored `raster` that is
let mut out = HashMap::new(); /// guaranteed to either be in luma8 or rgb8, and thus can be used for the
context.resources.traverse(&mut |resources| { /// `color_channel` method of `CustomImage`.
for (i, image) in resources.images.items().enumerate() { actual_dynamic: OnceLock<Arc<DynamicImage>>,
if out.contains_key(image) {
continue;
} }
let (handle, span) = resources.deferred_images.get(&i).unwrap(); impl PdfImage {
let encoded = handle.wait().as_ref().map_err(Clone::clone).at(*span)?; pub fn new(raster: RasterImage) -> Self {
Self {
match encoded { raster,
EncodedImage::Raster { alpha_channel: OnceLock::new(),
data, actual_dynamic: OnceLock::new(),
filter,
has_color,
width,
height,
icc,
alpha,
} => {
let image_ref = chunk.alloc();
out.insert(image.clone(), image_ref);
let mut image = chunk.chunk.image_xobject(image_ref, data);
image.filter(*filter);
image.width(*width as i32);
image.height(*height as i32);
image.bits_per_component(8);
let mut icc_ref = None;
let space = image.color_space();
if icc.is_some() {
let id = chunk.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
} else if *has_color {
color::write(
ColorSpace::Srgb,
space,
&context.globals.color_functions,
);
} else {
color::write(
ColorSpace::D65Gray,
space,
&context.globals.color_functions,
);
}
// Add a second gray-scale image containing the alpha values if
// this image has an alpha channel.
if let Some((alpha_data, alpha_filter)) = alpha {
let mask_ref = chunk.alloc.bump();
image.s_mask(mask_ref);
image.finish();
let mut mask = chunk.image_xobject(mask_ref, alpha_data);
mask.filter(*alpha_filter);
mask.width(*width as i32);
mask.height(*height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
} else {
image.finish();
}
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
let mut stream = chunk.icc_profile(icc_ref, icc);
stream.filter(Filter::FlateDecode);
if *has_color {
stream.n(3);
stream.alternate().srgb();
} else {
stream.n(1);
stream.alternate().d65_gray();
}
}
}
EncodedImage::Svg(svg_chunk, id) => {
let mut map = HashMap::new();
svg_chunk.renumber_into(&mut chunk.chunk, |old| {
*map.entry(old).or_insert_with(|| chunk.alloc.bump())
});
out.insert(image.clone(), map[id]);
} }
} }
} }
Ok(()) impl Hash for PdfImage {
})?; fn hash<H: Hasher>(&self, state: &mut H) {
/// `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`,
Ok((chunk, out)) /// so this is enough. Since `raster` is prehashed, this is also very cheap.
} self.raster.hash(state);
/// Creates a new PDF image from the given image.
///
/// Also starts the deferred encoding of the image.
#[comemo::memoize]
pub fn deferred_image(
image: Image,
pdfa: bool,
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
let color_space = match image.kind() {
ImageKind::Raster(raster) if raster.icc().is_none() => {
if raster.dynamic().color().channel_count() > 2 {
Some(ColorSpace::Srgb)
} else {
Some(ColorSpace::D65Gray)
} }
} }
_ => None,
};
let deferred = Deferred::new(move || match image.kind() { impl CustomImage for PdfImage {
ImageKind::Raster(raster) => { fn color_channel(&self) -> &[u8] {
let raster = raster.clone(); self.actual_dynamic
let (width, height) = (raster.width(), raster.height()); .get_or_init(|| {
let (data, filter, has_color) = encode_raster_image(&raster); let dynamic = self.raster.dynamic();
let icc = raster.icc().map(deflate); let channel_count = dynamic.color().channel_count();
let alpha = match (dynamic.as_ref(), channel_count) {
raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); // Pure luma8 or rgb8 image, can use it directly.
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
Ok(EncodedImage::Raster { (DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
data, // Grey-scale image, convert to luma8.
filter, (_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
has_color, // Anything else, convert to rgb8.
width, _ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
height, }
icc,
alpha,
}) })
} .as_bytes()
ImageKind::Svg(svg) => {
let (chunk, id) = encode_svg(svg, pdfa)
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
Ok(EncodedImage::Svg(chunk, id))
}
});
(deferred, color_space)
} }
/// Encode an image with a suitable filter and return the data, filter and fn alpha_channel(&self) -> Option<&[u8]> {
/// whether the image has color. self.alpha_channel
/// .get_or_init(|| {
/// Skips the alpha channel as that's encoded separately. self.raster.dynamic().color().has_alpha().then(|| {
#[typst_macros::time(name = "encode raster image")] Arc::new(
fn encode_raster_image(image: &RasterImage) -> (Vec<u8>, Filter, bool) { self.raster
// let dynamic = image.dynamic();
// let channel_count = dynamic.color().channel_count();
// let has_color = channel_count > 2;
//
// if image.format() == RasterFormat::Jpg {
// let mut data = Cursor::new(vec![]);
// dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
// (data.into_inner(), Filter::DctDecode, has_color)
// } else {
// // TODO: Encode flate streams with PNG-predictor?
// let data = match (dynamic, channel_count) {
// (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
// (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
// // Grayscale image
// (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()),
// // Anything else
// _ => deflate(dynamic.to_rgb8().as_raw()),
// };
// (data, Filter::FlateDecode, has_color)
// }
unimplemented!()
}
/// Encode an image's alpha channel if present.
#[typst_macros::time(name = "encode alpha")]
fn encode_alpha(raster: &RasterImage) -> (Vec<u8>, Filter) {
let pixels: Vec<_> = raster
.dynamic() .dynamic()
.pixels() .pixels()
.map(|(_, _, Rgba([_, _, _, a]))| a) .map(|(_, _, Rgba([_, _, _, a]))| a)
.collect(); .collect(),
(deflate(&pixels), Filter::FlateDecode) )
})
})
.as_ref()
.map(|v| &***v)
} }
/// Encode an SVG into a chunk of PDF objects. fn bits_per_component(&self) -> BitsPerComponent {
#[typst_macros::time(name = "encode svg")] BitsPerComponent::Eight
fn encode_svg(
svg: &SvgImage,
pdfa: bool,
) -> Result<(Chunk, Ref), svg2pdf::ConversionError> {
unimplemented!();
// svg2pdf::to_chunk(
// svg.tree(),
// svg2pdf::ConversionOptions { pdfa, ..Default::default() },
// )
} }
/// A pre-encoded image. fn size(&self) -> (u32, u32) {
pub enum EncodedImage { (self.raster.width(), self.raster.height())
/// A pre-encoded rasterized image. }
Raster {
/// The raw, pre-deflated image data. fn icc_profile(&self) -> Option<&[u8]> {
data: Vec<u8>, if matches!(
/// The filter to use for the image. self.raster.dynamic().as_ref(),
filter: Filter, DynamicImage::ImageLuma8(_)
/// Whether the image has color. | DynamicImage::ImageLumaA8(_)
has_color: bool, | DynamicImage::ImageRgb8(_)
/// The image's width. | DynamicImage::ImageRgba8(_)
width: u32, ) {
/// The image's height. self.raster.icc()
height: u32, } else {
/// The image's ICC profile, pre-deflated, if any. // In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC
icc: Option<Vec<u8>>, // profile may become invalid, and thus we don't include it.
/// The alpha channel of the image, pre-deflated, if any. None
alpha: Option<(Vec<u8>, Filter)>, }
}, }
/// A vector graphic.
/// fn color_space(&self) -> ImageColorspace {
/// The chunk is the SVG converted to PDF objects. if self.raster.dynamic().color().has_color() {
Svg(Chunk, Ref), ImageColorspace::Rgb
} else {
ImageColorspace::Luma
}
}
}
#[comemo::memoize]
pub(crate) fn raster(raster: RasterImage) -> Option<krilla::image::Image> {
match raster.format() {
RasterFormat::Jpg => {
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
}
_ => krilla::image::Image::from_custom(PdfImage::new(raster)),
}
} }

View File

@ -0,0 +1,240 @@
use std::collections::HashMap;
use std::io::Cursor;
use ecow::eco_format;
use image::{DynamicImage, GenericImageView, Rgba};
use pdf_writer::{Chunk, Filter, Finish, Ref};
use typst_library::diag::{At, SourceResult, StrResult};
use typst_library::visualize::{
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
};
use typst_utils::Deferred;
use crate::{color, deflate, PdfChunk, WithGlobalRefs};
/// Embed all used images into the PDF.
#[typst_macros::time(name = "write images")]
pub fn write_images(
context: &WithGlobalRefs,
) -> SourceResult<(PdfChunk, HashMap<Image, Ref>)> {
let mut chunk = PdfChunk::new();
let mut out = HashMap::new();
context.resources.traverse(&mut |resources| {
for (i, image) in resources.images.items().enumerate() {
if out.contains_key(image) {
continue;
}
let (handle, span) = resources.deferred_images.get(&i).unwrap();
let encoded = handle.wait().as_ref().map_err(Clone::clone).at(*span)?;
match encoded {
EncodedImage::Raster {
data,
filter,
has_color,
width,
height,
icc,
alpha,
} => {
let image_ref = chunk.alloc();
out.insert(image.clone(), image_ref);
let mut image = chunk.chunk.image_xobject(image_ref, data);
image.filter(*filter);
image.width(*width as i32);
image.height(*height as i32);
image.bits_per_component(8);
let mut icc_ref = None;
let space = image.color_space();
if icc.is_some() {
let id = chunk.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
} else if *has_color {
color::write(
ColorSpace::Srgb,
space,
&context.globals.color_functions,
);
} else {
color::write(
ColorSpace::D65Gray,
space,
&context.globals.color_functions,
);
}
// Add a second gray-scale image containing the alpha values if
// this image has an alpha channel.
if let Some((alpha_data, alpha_filter)) = alpha {
let mask_ref = chunk.alloc.bump();
image.s_mask(mask_ref);
image.finish();
let mut mask = chunk.image_xobject(mask_ref, alpha_data);
mask.filter(*alpha_filter);
mask.width(*width as i32);
mask.height(*height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
} else {
image.finish();
}
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
let mut stream = chunk.icc_profile(icc_ref, icc);
stream.filter(Filter::FlateDecode);
if *has_color {
stream.n(3);
stream.alternate().srgb();
} else {
stream.n(1);
stream.alternate().d65_gray();
}
}
}
EncodedImage::Svg(svg_chunk, id) => {
let mut map = HashMap::new();
svg_chunk.renumber_into(&mut chunk.chunk, |old| {
*map.entry(old).or_insert_with(|| chunk.alloc.bump())
});
out.insert(image.clone(), map[id]);
}
}
}
Ok(())
})?;
Ok((chunk, out))
}
/// Creates a new PDF image from the given image.
///
/// Also starts the deferred encoding of the image.
#[comemo::memoize]
pub fn deferred_image(
image: Image,
pdfa: bool,
) -> (Deferred<StrResult<EncodedImage>>, Option<ColorSpace>) {
let color_space = match image.kind() {
ImageKind::Raster(raster) if raster.icc().is_none() => {
if raster.dynamic().color().channel_count() > 2 {
Some(ColorSpace::Srgb)
} else {
Some(ColorSpace::D65Gray)
}
}
_ => None,
};
let deferred = Deferred::new(move || match image.kind() {
ImageKind::Raster(raster) => {
let raster = raster.clone();
let (width, height) = (raster.width(), raster.height());
let (data, filter, has_color) = encode_raster_image(&raster);
let icc = raster.icc().map(deflate);
let alpha =
raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster));
Ok(EncodedImage::Raster {
data,
filter,
has_color,
width,
height,
icc,
alpha,
})
}
ImageKind::Svg(svg) => {
let (chunk, id) = encode_svg(svg, pdfa)
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
Ok(EncodedImage::Svg(chunk, id))
}
});
(deferred, color_space)
}
/// Encode an image with a suitable filter and return the data, filter and
/// whether the image has color.
///
/// Skips the alpha channel as that's encoded separately.
#[typst_macros::time(name = "encode raster image")]
fn encode_raster_image(image: &RasterImage) -> (Vec<u8>, Filter, bool) {
// let dynamic = image.dynamic();
// let channel_count = dynamic.color().channel_count();
// let has_color = channel_count > 2;
//
// if image.format() == RasterFormat::Jpg {
// let mut data = Cursor::new(vec![]);
// dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
// (data.into_inner(), Filter::DctDecode, has_color)
// } else {
// // TODO: Encode flate streams with PNG-predictor?
// let data = match (dynamic, channel_count) {
// (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
// (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
// // Grayscale image
// (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()),
// // Anything else
// _ => deflate(dynamic.to_rgb8().as_raw()),
// };
// (data, Filter::FlateDecode, has_color)
// }
unimplemented!()
}
/// Encode an image's alpha channel if present.
#[typst_macros::time(name = "encode alpha")]
fn encode_alpha(raster: &RasterImage) -> (Vec<u8>, Filter) {
let pixels: Vec<_> = raster
.dynamic()
.pixels()
.map(|(_, _, Rgba([_, _, _, a]))| a)
.collect();
(deflate(&pixels), Filter::FlateDecode)
}
/// Encode an SVG into a chunk of PDF objects.
#[typst_macros::time(name = "encode svg")]
fn encode_svg(
svg: &SvgImage,
pdfa: bool,
) -> Result<(Chunk, Ref), svg2pdf::ConversionError> {
unimplemented!();
// svg2pdf::to_chunk(
// svg.tree(),
// svg2pdf::ConversionOptions { pdfa, ..Default::default() },
// )
}
/// A pre-encoded image.
pub enum EncodedImage {
/// A pre-encoded rasterized image.
Raster {
/// The raw, pre-deflated image data.
data: Vec<u8>,
/// The filter to use for the image.
filter: Filter,
/// Whether the image has color.
has_color: bool,
/// The image's width.
width: u32,
/// The image's height.
height: u32,
/// The image's ICC profile, pre-deflated, if any.
icc: Option<Vec<u8>>,
/// The alpha channel of the image, pre-deflated, if any.
alpha: Option<(Vec<u8>, Filter)>,
},
/// A vector graphic.
///
/// The chunk is the SVG converted to PDF objects.
Svg(Chunk, Ref),
}

View File

@ -1,14 +1,13 @@
use crate::AbsExt; use crate::{paint, primitive, AbsExt};
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use image::{DynamicImage, GenericImageView, Rgba}; use image::{DynamicImage, GenericImageView, Rgba};
use krilla::action::{Action, LinkAction}; use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target}; use krilla::annotation::{LinkAnnotation, Target};
use krilla::color::rgb;
use krilla::destination::XyzDestination; 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::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::path::{Fill, PathBuilder, Stroke}; use krilla::path::PathBuilder;
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::validation::Validator; use krilla::validation::Validator;
use krilla::version::PdfVersion; use krilla::version::PdfVersion;
@ -17,15 +16,14 @@ 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 svg2pdf::usvg::{NormalizedF32, Rect}; use svg2pdf::usvg::Rect;
use typst_library::layout::{Abs, Frame, FrameItem, GroupItem, Page, Size}; use typst_library::layout::{Abs, Frame, FrameItem, GroupItem, Page, Size};
use typst_library::model::{Destination, Document}; use typst_library::model::{Destination, Document};
use typst_library::text::{Font, Glyph, TextItem}; use typst_library::text::{Font, Glyph, TextItem};
use typst_library::visualize::{ use typst_library::visualize::{
ColorSpace, FillRule, FixedStroke, Geometry, Image, ImageKind, LineCap, LineJoin, FillRule, Geometry, Image, ImageKind, Path, PathItem, RasterFormat, RasterImage,
Paint, Path, PathItem, RasterFormat, RasterImage, Shape, Shape,
}; };
use typst_syntax::ast::Link;
#[derive(TransparentWrapper)] #[derive(TransparentWrapper)]
#[repr(transparent)] #[repr(transparent)]
@ -124,7 +122,7 @@ pub fn handle_group(
let old = context.cur_transform; let old = context.cur_transform;
context.cur_transform = context.cur_transform.pre_concat(group.transform); context.cur_transform = context.cur_transform.pre_concat(group.transform);
surface.push_transform(&convert_transform(group.transform)); surface.push_transform(&primitive::transform(group.transform));
process_frame(&group.frame, surface, context); process_frame(&group.frame, surface, context);
context.cur_transform = old; context.cur_transform = old;
@ -141,12 +139,7 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont
.unwrap() .unwrap()
}) })
.clone(); .clone();
let (paint, opacity) = convert_paint(&t.fill); let fill = paint::fill(&t.fill, FillRule::NonZero);
let fill = Fill {
paint,
opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(),
..Default::default()
};
let text = t.text.as_str(); let text = t.text.as_str();
let size = t.size; let size = t.size;
@ -163,7 +156,7 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont
false, false,
); );
if let Some(stroke) = t.stroke.as_ref().map(convert_fixed_stroke) { if let Some(stroke) = t.stroke.as_ref().map(paint::stroke) {
surface.stroke_glyphs( surface.stroke_glyphs(
Point::from_xy(0.0, 0.0), Point::from_xy(0.0, 0.0),
stroke, stroke,
@ -177,132 +170,24 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont
} }
} }
#[derive(Clone)]
struct PdfImage {
raster: RasterImage,
alpha_channel: OnceLock<Option<Arc<Vec<u8>>>>,
actual_dynamic: OnceLock<Arc<DynamicImage>>,
}
impl PdfImage {
pub fn new(raster: RasterImage) -> Self {
Self {
raster,
alpha_channel: OnceLock::new(),
actual_dynamic: OnceLock::new(),
}
}
}
impl Hash for PdfImage {
fn hash<H: Hasher>(&self, state: &mut H) {
self.raster.hash(state);
}
}
impl CustomImage for PdfImage {
fn color_channel(&self) -> &[u8] {
self.actual_dynamic
.get_or_init(|| {
let dynamic = self.raster.dynamic();
let channel_count = dynamic.color().channel_count();
match (dynamic.as_ref(), channel_count) {
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
(DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
(_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
_ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())),
}
})
.as_bytes()
}
fn alpha_channel(&self) -> Option<&[u8]> {
self.alpha_channel
.get_or_init(|| {
self.raster.dynamic().color().has_alpha().then(|| {
Arc::new(
self.raster
.dynamic()
.pixels()
.map(|(_, _, Rgba([_, _, _, a]))| a)
.collect(),
)
})
})
.as_ref()
.map(|v| &***v)
}
fn bits_per_component(&self) -> BitsPerComponent {
BitsPerComponent::Eight
}
fn size(&self) -> (u32, u32) {
(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 {
if self.raster.dynamic().color().has_color() {
ImageColorspace::Rgb
} else {
ImageColorspace::Luma
}
}
}
#[typst_macros::time(name = "handle image")]
pub fn handle_image( pub fn handle_image(
image: &Image, image: &Image,
size: &Size, size: Size,
surface: &mut Surface, surface: &mut Surface,
_: &mut ExportContext, _: &mut ExportContext,
) { ) {
match image.kind() { match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
let image = convert_raster(raster.clone()); // TODO: Don't unwrap
surface.draw_image( let image = crate::image::raster(raster.clone()).unwrap();
image, surface.draw_image(image, primitive::size(size));
krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(),
);
} }
ImageKind::Svg(svg) => { ImageKind::Svg(svg) => {
surface.draw_svg( surface.draw_svg(svg.tree(), primitive::size(size), SvgSettings::default());
svg.tree(),
krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(),
SvgSettings::default(),
);
} }
} }
} }
#[comemo::memoize]
fn convert_raster(raster: RasterImage) -> krilla::image::Image {
match raster.format() {
RasterFormat::Jpg => {
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
}
_ => 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) {
let mut path_builder = PathBuilder::new(); let mut path_builder = PathBuilder::new();
@ -323,19 +208,12 @@ pub fn handle_shape(shape: &Shape, surface: &mut Surface) {
if let Some(path) = path_builder.finish() { if let Some(path) = path_builder.finish() {
if let Some(paint) = &shape.fill { if let Some(paint) = &shape.fill {
let (paint, opacity) = convert_paint(paint); let fill = paint::fill(paint, shape.fill_rule);
let fill = Fill {
paint,
rule: convert_fill_rule(shape.fill_rule),
opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(),
};
surface.fill_path(&path, fill); surface.fill_path(&path, fill);
} }
if let Some(stroke) = &shape.stroke { if let Some(stroke) = &shape.stroke {
let stroke = convert_fixed_stroke(stroke); let stroke = paint::stroke(stroke);
surface.stroke_path(&path, stroke); surface.stroke_path(&path, stroke);
} }
} }
@ -370,8 +248,8 @@ pub fn process_frame(frame: &Frame, surface: &mut Surface, context: &mut ExportC
FrameItem::Group(g) => handle_group(g, surface, context), FrameItem::Group(g) => handle_group(g, surface, context),
FrameItem::Text(t) => handle_text(t, surface, context), FrameItem::Text(t) => handle_text(t, surface, context),
FrameItem::Shape(s, _) => handle_shape(s, surface), FrameItem::Shape(s, _) => handle_shape(s, surface),
FrameItem::Image(image, size, _) => { FrameItem::Image(image, size, span) => {
handle_image(image, size, surface, context) handle_image(image, *size, surface, context)
} }
FrameItem::Link(d, s) => handle_link(*point, d, *s, context, surface), FrameItem::Link(d, s) => handle_link(*point, d, *s, context, surface),
FrameItem::Tag(_) => {} FrameItem::Tag(_) => {}
@ -419,7 +297,7 @@ fn handle_link(
} }
Destination::Position(p) => { Destination::Position(p) => {
Target::Destination(krilla::destination::Destination::Xyz( Target::Destination(krilla::destination::Destination::Xyz(
XyzDestination::new(p.page.get() - 1, convert_point(p.point)), XyzDestination::new(p.page.get() - 1, primitive::point(p.point)),
)) ))
} }
Destination::Location(_) => return, Destination::Location(_) => return,
@ -427,68 +305,3 @@ fn handle_link(
ctx.annotations.push(LinkAnnotation::new(rect, target).into()); ctx.annotations.push(LinkAnnotation::new(rect, target).into());
} }
fn convert_fill_rule(fill_rule: FillRule) -> krilla::path::FillRule {
match fill_rule {
FillRule::NonZero => krilla::path::FillRule::NonZero,
FillRule::EvenOdd => krilla::path::FillRule::EvenOdd,
}
}
fn convert_fixed_stroke(stroke: &FixedStroke) -> Stroke {
let (paint, opacity) = convert_paint(&stroke.paint);
Stroke {
paint,
width: stroke.thickness.to_f32(),
miter_limit: stroke.miter_limit.get() as f32,
line_join: convert_linejoin(stroke.join),
line_cap: convert_linecap(stroke.cap),
opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(),
..Default::default()
}
}
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 {
match l {
LineCap::Butt => krilla::path::LineCap::Butt,
LineCap::Round => krilla::path::LineCap::Round,
LineCap::Square => krilla::path::LineCap::Square,
}
}
fn convert_linejoin(l: LineJoin) -> krilla::path::LineJoin {
match l {
LineJoin::Miter => krilla::path::LineJoin::Miter,
LineJoin::Round => krilla::path::LineJoin::Round,
LineJoin::Bevel => krilla::path::LineJoin::Bevel,
}
}
fn convert_transform(t: crate::Transform) -> krilla::geom::Transform {
Transform::from_row(
t.sx.get() as f32,
t.ky.get() as f32,
t.kx.get() as f32,
t.sy.get() as f32,
t.tx.to_f32(),
t.ty.to_f32(),
)
}
fn convert_paint(paint: &Paint) -> (krilla::paint::Paint, u8) {
match paint {
Paint::Solid(c) => {
let components = c.to_space(ColorSpace::Srgb).to_vec4_u8();
(
rgb::Color::new(components[0], components[1], components[2]).into(),
components[3],
)
}
Paint::Gradient(_) => (rgb::Color::black().into(), 255),
Paint::Pattern(_) => (rgb::Color::black().into(), 255),
}
}

View File

@ -8,11 +8,14 @@ mod extg;
mod font; mod font;
mod gradient; mod gradient;
mod image; mod image;
mod image_old;
mod krilla; mod krilla;
mod named_destination; mod named_destination;
mod outline; mod outline;
mod page; mod page;
mod paint;
mod pattern; mod pattern;
mod primitive;
mod resources; mod resources;
use std::collections::HashMap; use std::collections::HashMap;
@ -38,7 +41,7 @@ use crate::color_font::{write_color_fonts, ColorFontSlice};
use crate::extg::{write_graphic_states, ExtGState}; use crate::extg::{write_graphic_states, ExtGState};
use crate::font::write_fonts; use crate::font::write_fonts;
use crate::gradient::{write_gradients, PdfGradient}; use crate::gradient::{write_gradients, PdfGradient};
use crate::image::write_images; use crate::image_old::write_images;
use crate::named_destination::{write_named_destinations, NamedDestinations}; use crate::named_destination::{write_named_destinations, NamedDestinations};
use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage}; use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage};
use crate::pattern::{write_patterns, PdfPattern}; use crate::pattern::{write_patterns, PdfPattern};

View File

@ -0,0 +1,57 @@
//! Convert paint types from typst to krilla.
use krilla::geom::NormalizedF32;
use typst_library::visualize::{ColorSpace, FillRule, FixedStroke, Paint};
use crate::primitive::{linecap, linejoin};
use crate::AbsExt;
pub(crate) fn fill(paint_: &Paint, fill_rule_: FillRule) -> krilla::path::Fill {
let (paint, opacity) = paint(paint_);
krilla::path::Fill {
paint,
rule: fill_rule(fill_rule_),
opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(),
}
}
pub(crate) fn stroke(stroke: &FixedStroke) -> krilla::path::Stroke {
let (paint, opacity) = paint(&stroke.paint);
krilla::path::Stroke {
paint,
width: stroke.thickness.to_f32(),
miter_limit: stroke.miter_limit.get() as f32,
line_join: linejoin(stroke.join),
line_cap: linecap(stroke.cap),
opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(),
// TODO: Convert dash
dash: None,
}
}
fn paint(paint: &Paint) -> (krilla::paint::Paint, u8) {
match paint {
Paint::Solid(c) => {
let components = c.to_space(ColorSpace::Srgb).to_vec4_u8();
(
krilla::color::rgb::Color::new(
components[0],
components[1],
components[2],
)
.into(),
components[3],
)
}
Paint::Gradient(_) => (krilla::color::rgb::Color::black().into(), 255),
Paint::Pattern(_) => (krilla::color::rgb::Color::black().into(), 255),
}
}
fn fill_rule(fill_rule: FillRule) -> krilla::path::FillRule {
match fill_rule {
FillRule::NonZero => krilla::path::FillRule::NonZero,
FillRule::EvenOdd => krilla::path::FillRule::EvenOdd,
}
}

View File

@ -0,0 +1,41 @@
//! Convert basic primitive types from typst to krilla.
use typst_library::layout::{Point, Size, Transform};
use typst_library::visualize::{LineCap, LineJoin};
use crate::AbsExt;
pub(crate) fn size(s: Size) -> krilla::geom::Size {
krilla::geom::Size::from_wh(s.x.to_f32(), s.y.to_f32()).unwrap()
}
pub(crate) fn point(p: Point) -> krilla::geom::Point {
krilla::geom::Point::from_xy(p.x.to_f32(), p.y.to_f32())
}
pub(crate) fn linecap(l: LineCap) -> krilla::path::LineCap {
match l {
LineCap::Butt => krilla::path::LineCap::Butt,
LineCap::Round => krilla::path::LineCap::Round,
LineCap::Square => krilla::path::LineCap::Square,
}
}
pub(crate) fn linejoin(l: LineJoin) -> krilla::path::LineJoin {
match l {
LineJoin::Miter => krilla::path::LineJoin::Miter,
LineJoin::Round => krilla::path::LineJoin::Round,
LineJoin::Bevel => krilla::path::LineJoin::Bevel,
}
}
pub(crate) fn transform(t: Transform) -> krilla::geom::Transform {
krilla::geom::Transform::from_row(
t.sx.get() as f32,
t.ky.get() as f32,
t.kx.get() as f32,
t.sy.get() as f32,
t.tx.to_f32(),
t.ty.to_f32(),
)
}

View File

@ -22,7 +22,7 @@ use crate::color::ColorSpaces;
use crate::color_font::ColorFontMap; use crate::color_font::ColorFontMap;
use crate::extg::ExtGState; use crate::extg::ExtGState;
use crate::gradient::PdfGradient; use crate::gradient::PdfGradient;
use crate::image::EncodedImage; use crate::image_old::EncodedImage;
use crate::pattern::PatternRemapper; use crate::pattern::PatternRemapper;
use crate::{PdfChunk, Renumber, WithEverything, WithResources}; use crate::{PdfChunk, Renumber, WithEverything, WithResources};