mirror of
https://github.com/typst/typst
synced 2025-06-09 13:46:24 +08:00
more
This commit is contained in:
parent
15faecbf27
commit
849994c827
@ -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};
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
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.
|
impl PdfImage {
|
||||||
///
|
pub fn new(raster: RasterImage) -> Self {
|
||||||
/// Also starts the deferred encoding of the image.
|
Self {
|
||||||
#[comemo::memoize]
|
raster,
|
||||||
pub fn deferred_image(
|
alpha_channel: OnceLock::new(),
|
||||||
image: Image,
|
actual_dynamic: OnceLock::new(),
|
||||||
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 Hash for PdfImage {
|
||||||
ImageKind::Raster(raster) => {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
let raster = raster.clone();
|
/// `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`,
|
||||||
let (width, height) = (raster.width(), raster.height());
|
/// so this is enough. Since `raster` is prehashed, this is also very cheap.
|
||||||
let (data, filter, has_color) = encode_raster_image(&raster);
|
self.raster.hash(state);
|
||||||
let icc = raster.icc().map(deflate);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let alpha =
|
impl CustomImage for PdfImage {
|
||||||
raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster));
|
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 {
|
match (dynamic.as_ref(), channel_count) {
|
||||||
data,
|
// Pure luma8 or rgb8 image, can use it directly.
|
||||||
filter,
|
(DynamicImage::ImageLuma8(_), _) => dynamic.clone(),
|
||||||
has_color,
|
(DynamicImage::ImageRgb8(_), _) => dynamic.clone(),
|
||||||
width,
|
// Grey-scale image, convert to luma8.
|
||||||
height,
|
(_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())),
|
||||||
icc,
|
// Anything else, convert to rgb8.
|
||||||
alpha,
|
_ => 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}"))?;
|
fn color_space(&self) -> ImageColorspace {
|
||||||
Ok(EncodedImage::Svg(chunk, id))
|
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
|
#[comemo::memoize]
|
||||||
/// whether the image has color.
|
pub(crate) fn raster(raster: RasterImage) -> Option<krilla::image::Image> {
|
||||||
///
|
match raster.format() {
|
||||||
/// Skips the alpha channel as that's encoded separately.
|
RasterFormat::Jpg => {
|
||||||
#[typst_macros::time(name = "encode raster image")]
|
krilla::image::Image::from_jpeg(Arc::new(raster.data().clone()))
|
||||||
fn encode_raster_image(image: &RasterImage) -> (Vec<u8>, Filter, bool) {
|
}
|
||||||
// let dynamic = image.dynamic();
|
_ => krilla::image::Image::from_custom(PdfImage::new(raster)),
|
||||||
// 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),
|
|
||||||
}
|
}
|
||||||
|
240
crates/typst-pdf/src/image_old.rs
Normal file
240
crates/typst-pdf/src/image_old.rs
Normal 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),
|
||||||
|
}
|
@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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};
|
||||||
|
57
crates/typst-pdf/src/paint.rs
Normal file
57
crates/typst-pdf/src/paint.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
41
crates/typst-pdf/src/primitive.rs
Normal file
41
crates/typst-pdf/src/primitive.rs
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
@ -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};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user