diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index ce72365dd..a6dc59608 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -28,7 +28,7 @@ use typst_utils::{Deferred, Numeric, SliceExt}; use crate::color::PaintEncode; use crate::color_font::ColorFontMap; use crate::extg::ExtGState; -use crate::image::deferred_image; +use crate::image_old::deferred_image; use crate::resources::Resources; use crate::{deflate_deferred, AbsExt, ContentExt, EmExt, PdfOptions, StrExt}; diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index b3b1cd914..eba257f3d 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -1,240 +1,116 @@ -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 krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, OnceLock}; +use typst_library::visualize::{RasterFormat, RasterImage}; -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)> { - 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)) +/// A wrapper around RasterImage so that we can implement `CustomImage`. +#[derive(Clone)] +struct PdfImage { + /// The original, underlying raster image. + raster: RasterImage, + /// The alpha channel of the raster image, if existing. + alpha_channel: OnceLock>>>, + /// A (potentially) converted version of the dynamic image stored `raster` that is + /// guaranteed to either be in luma8 or rgb8, and thus can be used for the + /// `color_channel` method of `CustomImage`. + actual_dynamic: OnceLock>, } -/// 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>, Option) { - 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) - } +impl PdfImage { + pub fn new(raster: RasterImage) -> Self { + Self { + raster, + alpha_channel: OnceLock::new(), + actual_dynamic: OnceLock::new(), } - _ => 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); +impl Hash for PdfImage { + fn hash(&self, state: &mut H) { + /// `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`, + /// so this is enough. Since `raster` is prehashed, this is also very cheap. + self.raster.hash(state); + } +} - let alpha = - raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); +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(); - Ok(EncodedImage::Raster { - data, - filter, - has_color, - width, - height, - icc, - alpha, + match (dynamic.as_ref(), channel_count) { + // Pure luma8 or rgb8 image, can use it directly. + (DynamicImage::ImageLuma8(_), _) => dynamic.clone(), + (DynamicImage::ImageRgb8(_), _) => dynamic.clone(), + // Grey-scale image, convert to luma8. + (_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())), + // Anything else, convert to rgb8. + _ => 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 or LUMA8, so the ICC + // profile may become invalid, and thus we don't include it. + None } - 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)) + } + + fn color_space(&self) -> ImageColorspace { + if self.raster.dynamic().color().has_color() { + ImageColorspace::Rgb + } else { + ImageColorspace::Luma } - }); - - (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, 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, 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, - /// 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>, - /// The alpha channel of the image, pre-deflated, if any. - alpha: Option<(Vec, Filter)>, - }, - /// A vector graphic. - /// - /// The chunk is the SVG converted to PDF objects. - Svg(Chunk, Ref), +#[comemo::memoize] +pub(crate) fn raster(raster: RasterImage) -> Option { + match raster.format() { + RasterFormat::Jpg => { + krilla::image::Image::from_jpeg(Arc::new(raster.data().clone())) + } + _ => krilla::image::Image::from_custom(PdfImage::new(raster)), + } } diff --git a/crates/typst-pdf/src/image_old.rs b/crates/typst-pdf/src/image_old.rs new file mode 100644 index 000000000..b3b1cd914 --- /dev/null +++ b/crates/typst-pdf/src/image_old.rs @@ -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)> { + 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>, Option) { + 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, 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, 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, + /// 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>, + /// The alpha channel of the image, pre-deflated, if any. + alpha: Option<(Vec, Filter)>, + }, + /// A vector graphic. + /// + /// The chunk is the SVG converted to PDF objects. + Svg(Chunk, Ref), +} diff --git a/crates/typst-pdf/src/krilla.rs b/crates/typst-pdf/src/krilla.rs index 7071f928c..113bfdcc8 100644 --- a/crates/typst-pdf/src/krilla.rs +++ b/crates/typst-pdf/src/krilla.rs @@ -1,14 +1,13 @@ -use crate::AbsExt; +use crate::{paint, primitive, 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::destination::XyzDestination; use krilla::font::{GlyphId, GlyphUnits}; use krilla::geom::{Point, Transform}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; -use krilla::path::{Fill, PathBuilder, Stroke}; +use krilla::path::PathBuilder; use krilla::surface::Surface; use krilla::validation::Validator; use krilla::version::PdfVersion; @@ -17,15 +16,14 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::ops::Range; 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::model::{Destination, Document}; 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, + FillRule, Geometry, Image, ImageKind, Path, PathItem, RasterFormat, RasterImage, + Shape, }; -use typst_syntax::ast::Link; #[derive(TransparentWrapper)] #[repr(transparent)] @@ -124,7 +122,7 @@ pub fn handle_group( 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(&primitive::transform(group.transform)); process_frame(&group.frame, surface, context); context.cur_transform = old; @@ -141,12 +139,7 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont .unwrap() }) .clone(); - let (paint, opacity) = convert_paint(&t.fill); - let fill = Fill { - paint, - opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), - ..Default::default() - }; + let fill = paint::fill(&t.fill, FillRule::NonZero); let text = t.text.as_str(); let size = t.size; @@ -163,7 +156,7 @@ pub fn handle_text(t: &TextItem, surface: &mut Surface, context: &mut ExportCont 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( Point::from_xy(0.0, 0.0), 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>>>, - actual_dynamic: OnceLock>, -} - -impl PdfImage { - pub fn new(raster: RasterImage) -> Self { - Self { - raster, - alpha_channel: OnceLock::new(), - actual_dynamic: OnceLock::new(), - } - } -} - -impl Hash for PdfImage { - fn hash(&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( image: &Image, - size: &Size, + size: Size, surface: &mut Surface, _: &mut ExportContext, ) { match image.kind() { ImageKind::Raster(raster) => { - let image = convert_raster(raster.clone()); - surface.draw_image( - image, - krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(), - ); + // TODO: Don't unwrap + let image = crate::image::raster(raster.clone()).unwrap(); + surface.draw_image(image, primitive::size(size)); } ImageKind::Svg(svg) => { - surface.draw_svg( - svg.tree(), - krilla::geom::Size::from_wh(size.x.to_f32(), size.y.to_f32()).unwrap(), - SvgSettings::default(), - ); + surface.draw_svg(svg.tree(), primitive::size(size), 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) { 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(paint) = &shape.fill { - let (paint, opacity) = convert_paint(paint); - - let fill = Fill { - paint, - rule: convert_fill_rule(shape.fill_rule), - opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), - }; + let fill = paint::fill(paint, shape.fill_rule); surface.fill_path(&path, fill); } if let Some(stroke) = &shape.stroke { - let stroke = convert_fixed_stroke(stroke); - + let stroke = paint::stroke(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::Text(t) => handle_text(t, surface, context), FrameItem::Shape(s, _) => handle_shape(s, surface), - FrameItem::Image(image, size, _) => { - handle_image(image, size, surface, context) + FrameItem::Image(image, size, span) => { + handle_image(image, *size, surface, context) } FrameItem::Link(d, s) => handle_link(*point, d, *s, context, surface), FrameItem::Tag(_) => {} @@ -419,7 +297,7 @@ fn handle_link( } Destination::Position(p) => { 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, @@ -427,68 +305,3 @@ fn handle_link( 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), - } -} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 4c0d52c14..4b3091d9c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -8,11 +8,14 @@ mod extg; mod font; mod gradient; mod image; +mod image_old; mod krilla; mod named_destination; mod outline; mod page; +mod paint; mod pattern; +mod primitive; mod resources; 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::font::write_fonts; 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::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage}; use crate::pattern::{write_patterns, PdfPattern}; diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs new file mode 100644 index 000000000..6362f000e --- /dev/null +++ b/crates/typst-pdf/src/paint.rs @@ -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, + } +} diff --git a/crates/typst-pdf/src/primitive.rs b/crates/typst-pdf/src/primitive.rs new file mode 100644 index 000000000..b91f253f1 --- /dev/null +++ b/crates/typst-pdf/src/primitive.rs @@ -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(), + ) +} diff --git a/crates/typst-pdf/src/resources.rs b/crates/typst-pdf/src/resources.rs index d3fde5dd7..12aa87e6e 100644 --- a/crates/typst-pdf/src/resources.rs +++ b/crates/typst-pdf/src/resources.rs @@ -22,7 +22,7 @@ use crate::color::ColorSpaces; use crate::color_font::ColorFontMap; use crate::extg::ExtGState; use crate::gradient::PdfGradient; -use crate::image::EncodedImage; +use crate::image_old::EncodedImage; use crate::pattern::PatternRemapper; use crate::{PdfChunk, Renumber, WithEverything, WithResources};