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_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};
|
||||
|
||||
|
@ -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<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))
|
||||
/// 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<Option<Arc<Vec<u8>>>>,
|
||||
/// 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<Arc<DynamicImage>>,
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
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<H: Hasher>(&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<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),
|
||||
#[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)),
|
||||
}
|
||||
}
|
||||
|
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 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<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(
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
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::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};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user