feat: do not re-encode JPEG images

This commit is contained in:
frozolotl 2024-12-28 14:13:20 +01:00
parent db9a83d9fc
commit 12a6d6cb4d

View File

@ -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!(),
}
}