mirror of
https://github.com/typst/typst
synced 2025-08-14 15:17:57 +08:00
feat: do not re-encode JPEG images
This commit is contained in:
parent
db9a83d9fc
commit
12a6d6cb4d
@ -1,8 +1,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Cursor;
|
|
||||||
|
|
||||||
use ecow::eco_format;
|
use ecow::eco_format;
|
||||||
use image::{DynamicImage, GenericImageView, Rgba};
|
use image::{DynamicImage, GenericImageView, LumaA, Rgba};
|
||||||
use pdf_writer::{Chunk, Filter, Finish, Ref};
|
use pdf_writer::{Chunk, Filter, Finish, Ref};
|
||||||
use typst_library::diag::{At, SourceResult, StrResult};
|
use typst_library::diag::{At, SourceResult, StrResult};
|
||||||
use typst_library::foundations::Smart;
|
use typst_library::foundations::Smart;
|
||||||
@ -138,7 +137,12 @@ pub fn deferred_image(
|
|||||||
let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
|
let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth);
|
||||||
|
|
||||||
let deferred = Deferred::new(move || match image.kind() {
|
let deferred = Deferred::new(move || match image.kind() {
|
||||||
ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)),
|
ImageKind::Raster(raster)
|
||||||
|
if raster.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) =>
|
||||||
|
{
|
||||||
|
Ok(encode_raster_jpeg(raster, interpolate))
|
||||||
|
}
|
||||||
|
ImageKind::Raster(raster) => Ok(encode_raster_flate(raster, interpolate)),
|
||||||
ImageKind::Svg(svg) => {
|
ImageKind::Svg(svg) => {
|
||||||
let (chunk, id) = encode_svg(svg, pdfa)
|
let (chunk, id) = encode_svg(svg, pdfa)
|
||||||
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
|
.map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?;
|
||||||
@ -149,41 +153,57 @@ pub fn deferred_image(
|
|||||||
(deferred, color_space)
|
(deferred, color_space)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode an image with a suitable filter.
|
/// Include the source image's JPEG data without re-encoding.
|
||||||
#[typst_macros::time(name = "encode raster image")]
|
fn encode_raster_jpeg(image: &RasterImage, interpolate: bool) -> EncodedImage {
|
||||||
fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
|
let dynamic = image.dynamic();
|
||||||
|
|
||||||
|
let color_type = dynamic.color();
|
||||||
|
let color_space = to_color_space(color_type);
|
||||||
|
let bits_per_component = bits_per_component(color_type);
|
||||||
|
|
||||||
|
let compressed_icc = image.icc().map(|bytes| deflate(bytes.as_ref()));
|
||||||
|
let alpha = encode_alpha(dynamic);
|
||||||
|
|
||||||
|
EncodedImage::Raster {
|
||||||
|
data: image.data().to_vec(),
|
||||||
|
filter: Filter::DctDecode,
|
||||||
|
color_space,
|
||||||
|
bits_per_component,
|
||||||
|
width: dynamic.width(),
|
||||||
|
height: dynamic.height(),
|
||||||
|
compressed_icc,
|
||||||
|
alpha,
|
||||||
|
interpolate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode an arbitrary raster image with a suitable filter.
|
||||||
|
#[typst_macros::time(name = "encode raster image flate")]
|
||||||
|
fn encode_raster_flate(image: &RasterImage, interpolate: bool) -> EncodedImage {
|
||||||
let dynamic = image.dynamic();
|
let dynamic = image.dynamic();
|
||||||
let color_space = to_color_space(dynamic.color());
|
let color_space = to_color_space(dynamic.color());
|
||||||
|
let bits_per_component = bits_per_component(dynamic.color());
|
||||||
|
|
||||||
let (filter, data, bits_per_component) =
|
// TODO: Encode flate streams with PNG-predictor?
|
||||||
if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) {
|
let data = match (dynamic, color_space) {
|
||||||
let mut data = Cursor::new(vec![]);
|
(DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()),
|
||||||
dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap();
|
// Grayscale image
|
||||||
(Filter::DctDecode, data.into_inner(), 8)
|
(DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()),
|
||||||
} else {
|
(_, ColorSpace::D65Gray) => deflate(dynamic.to_luma8().as_raw()),
|
||||||
// TODO: Encode flate streams with PNG-predictor?
|
// Anything else
|
||||||
let (data, bits_per_component) = match (dynamic, color_space) {
|
_ => deflate(dynamic.to_rgb8().as_raw()),
|
||||||
// RGB image.
|
};
|
||||||
(DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8),
|
|
||||||
// Grayscale image
|
|
||||||
(DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8),
|
|
||||||
(_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8),
|
|
||||||
// Anything else
|
|
||||||
_ => (deflate(dynamic.to_rgb8().as_raw()), 8),
|
|
||||||
};
|
|
||||||
(Filter::FlateDecode, data, bits_per_component)
|
|
||||||
};
|
|
||||||
|
|
||||||
let compressed_icc = image.icc().map(|data| deflate(data));
|
let compressed_icc = image.icc().map(|bytes| deflate(bytes.as_ref()));
|
||||||
let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic));
|
let alpha = encode_alpha(dynamic);
|
||||||
|
|
||||||
EncodedImage::Raster {
|
EncodedImage::Raster {
|
||||||
data,
|
data,
|
||||||
filter,
|
filter: Filter::FlateDecode,
|
||||||
color_space,
|
color_space,
|
||||||
bits_per_component,
|
bits_per_component,
|
||||||
width: image.width(),
|
width: dynamic.width(),
|
||||||
height: image.height(),
|
height: dynamic.height(),
|
||||||
compressed_icc,
|
compressed_icc,
|
||||||
alpha,
|
alpha,
|
||||||
interpolate,
|
interpolate,
|
||||||
@ -192,9 +212,27 @@ fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage {
|
|||||||
|
|
||||||
/// Encode an image's alpha channel if present.
|
/// Encode an image's alpha channel if present.
|
||||||
#[typst_macros::time(name = "encode alpha")]
|
#[typst_macros::time(name = "encode alpha")]
|
||||||
fn encode_alpha(image: &DynamicImage) -> (Vec<u8>, Filter) {
|
fn encode_alpha(image: &DynamicImage) -> Option<(Vec<u8>, Filter)> {
|
||||||
let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
|
if !image.color().has_alpha() {
|
||||||
(deflate(&pixels), Filter::FlateDecode)
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the alpha channel as big-endian.
|
||||||
|
let alpha: Vec<u8> = match image {
|
||||||
|
DynamicImage::ImageLumaA8(buf) => buf.pixels().map(|&LumaA([_, a])| a).collect(),
|
||||||
|
DynamicImage::ImageLumaA16(buf) => {
|
||||||
|
buf.pixels().flat_map(|&LumaA([_, a])| a.to_be_bytes()).collect()
|
||||||
|
}
|
||||||
|
DynamicImage::ImageRgba16(buf) => {
|
||||||
|
buf.pixels().flat_map(|&Rgba([_, _, _, a])| a.to_be_bytes()).collect()
|
||||||
|
}
|
||||||
|
DynamicImage::ImageRgba32F(buf) => {
|
||||||
|
buf.pixels().flat_map(|&Rgba([_, _, _, a])| a.to_be_bytes()).collect()
|
||||||
|
}
|
||||||
|
// Everything else is encoded as RGBA8.
|
||||||
|
_ => image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(),
|
||||||
|
};
|
||||||
|
Some((deflate(&alpha), Filter::FlateDecode))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode an SVG into a chunk of PDF objects.
|
/// Encode an SVG into a chunk of PDF objects.
|
||||||
@ -209,6 +247,27 @@ fn encode_svg(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Matches an [`image::ColorType`] to [`ColorSpace`].
|
||||||
|
fn to_color_space(color: image::ColorType) -> ColorSpace {
|
||||||
|
use image::ColorType::*;
|
||||||
|
match color {
|
||||||
|
L8 | La8 | L16 | La16 => ColorSpace::D65Gray,
|
||||||
|
Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many bits does each component take up?
|
||||||
|
fn bits_per_component(color: image::ColorType) -> u8 {
|
||||||
|
use image::ColorType::*;
|
||||||
|
match color {
|
||||||
|
Rgb8 | Rgba8 | L8 | La8 => 8,
|
||||||
|
Rgb16 | Rgba16 | L16 | La16 => 16,
|
||||||
|
Rgb32F | Rgba32F => 32,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A pre-encoded image.
|
/// A pre-encoded image.
|
||||||
pub enum EncodedImage {
|
pub enum EncodedImage {
|
||||||
/// A pre-encoded rasterized image.
|
/// A pre-encoded rasterized image.
|
||||||
@ -237,13 +296,3 @@ pub enum EncodedImage {
|
|||||||
/// The chunk is the SVG converted to PDF objects.
|
/// The chunk is the SVG converted to PDF objects.
|
||||||
Svg(Chunk, Ref),
|
Svg(Chunk, Ref),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Matches an [`image::ColorType`] to [`ColorSpace`].
|
|
||||||
fn to_color_space(color: image::ColorType) -> ColorSpace {
|
|
||||||
use image::ColorType::*;
|
|
||||||
match color {
|
|
||||||
L8 | La8 | L16 | La16 => ColorSpace::D65Gray,
|
|
||||||
Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb,
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user