Nuked custom PDF Oklab colorspace code (#4871)

This commit is contained in:
Sébastien d'Herbais de Thun 2024-09-02 15:16:33 +02:00 committed by GitHub
parent 51df7aee76
commit ecad396cc8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 33 additions and 177 deletions

View File

@ -1,6 +1,6 @@
use arrayvec::ArrayVec; use arrayvec::ArrayVec;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pdf_writer::{types::DeviceNSubtype, writers, Chunk, Dict, Filter, Name, Ref}; use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
use typst::visualize::{Color, ColorSpace, Paint}; use typst::visualize::{Color, ColorSpace, Paint};
use crate::{content, deflate, PdfChunk, Renumber, WithResources}; use crate::{content, deflate, PdfChunk, Renumber, WithResources};
@ -8,28 +8,17 @@ use crate::{content, deflate, PdfChunk, Renumber, WithResources};
// The names of the color spaces. // The names of the color spaces.
pub const SRGB: Name<'static> = Name(b"srgb"); pub const SRGB: Name<'static> = Name(b"srgb");
pub const D65_GRAY: Name<'static> = Name(b"d65gray"); pub const D65_GRAY: Name<'static> = Name(b"d65gray");
pub const OKLAB: Name<'static> = Name(b"oklab");
pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb");
// The names of the color components.
const OKLAB_L: Name<'static> = Name(b"L");
const OKLAB_A: Name<'static> = Name(b"A");
const OKLAB_B: Name<'static> = Name(b"B");
// The ICC profiles. // The ICC profiles.
static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> = static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(typst_assets::icc::S_RGB_V4)); Lazy::new(|| deflate(typst_assets::icc::S_RGB_V4));
static GRAY_ICC_DEFLATED: Lazy<Vec<u8>> = static GRAY_ICC_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(typst_assets::icc::S_GREY_V4)); Lazy::new(|| deflate(typst_assets::icc::S_GREY_V4));
// The PostScript functions for color spaces.
static OKLAB_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(minify(include_str!("oklab.ps")).as_bytes()));
/// The color spaces present in the PDF document /// The color spaces present in the PDF document
#[derive(Default)] #[derive(Default)]
pub struct ColorSpaces { pub struct ColorSpaces {
use_oklab: bool,
use_srgb: bool, use_srgb: bool,
use_d65_gray: bool, use_d65_gray: bool,
use_linear_rgb: bool, use_linear_rgb: bool,
@ -39,11 +28,11 @@ impl ColorSpaces {
/// Mark a color space as used. /// Mark a color space as used.
pub fn mark_as_used(&mut self, color_space: ColorSpace) { pub fn mark_as_used(&mut self, color_space: ColorSpace) {
match color_space { match color_space {
ColorSpace::Oklch | ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => { ColorSpace::Oklch
self.use_oklab = true; | ColorSpace::Oklab
self.use_linear_rgb = true; | ColorSpace::Hsl
} | ColorSpace::Hsv
ColorSpace::Srgb => { | ColorSpace::Srgb => {
self.use_srgb = true; self.use_srgb = true;
} }
ColorSpace::D65Gray => { ColorSpace::D65Gray => {
@ -58,10 +47,6 @@ impl ColorSpaces {
/// Write the color spaces to the PDF file. /// Write the color spaces to the PDF file.
pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) { pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) {
if self.use_oklab {
write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), refs);
}
if self.use_srgb { if self.use_srgb {
write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs); write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs);
} }
@ -78,15 +63,6 @@ impl ColorSpaces {
/// Write the necessary color spaces functions and ICC profiles to the /// Write the necessary color spaces functions and ICC profiles to the
/// PDF file. /// PDF file.
pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) { pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) {
// Write the Oklab function & color space.
if self.use_oklab {
chunk
.post_script_function(refs.oklab.unwrap(), &OKLAB_DEFLATED)
.domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.filter(Filter::FlateDecode);
}
// Write the sRGB color space. // Write the sRGB color space.
if self.use_srgb { if self.use_srgb {
chunk chunk
@ -111,7 +87,6 @@ impl ColorSpaces {
pub fn merge(&mut self, other: &Self) { pub fn merge(&mut self, other: &Self) {
self.use_d65_gray |= other.use_d65_gray; self.use_d65_gray |= other.use_d65_gray;
self.use_linear_rgb |= other.use_linear_rgb; self.use_linear_rgb |= other.use_linear_rgb;
self.use_oklab |= other.use_oklab;
self.use_srgb |= other.use_srgb; self.use_srgb |= other.use_srgb;
} }
} }
@ -123,14 +98,11 @@ pub fn write(
refs: &ColorFunctionRefs, refs: &ColorFunctionRefs,
) { ) {
match color_space { match color_space {
ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => { ColorSpace::Srgb
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); | ColorSpace::Oklab
write(ColorSpace::LinearRgb, oklab.alternate_color_space(), refs); | ColorSpace::Hsl
oklab.tint_ref(refs.oklab.unwrap()); | ColorSpace::Hsv
oklab.attrs().subtype(DeviceNSubtype::DeviceN); | ColorSpace::Oklch => writer.icc_based(refs.srgb.unwrap()),
}
ColorSpace::Oklch => write(ColorSpace::Oklab, writer, refs),
ColorSpace::Srgb => writer.icc_based(refs.srgb.unwrap()),
ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()), ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()),
ColorSpace::LinearRgb => { ColorSpace::LinearRgb => {
writer.cal_rgb( writer.cal_rgb(
@ -152,16 +124,12 @@ pub fn write(
/// needed) in the final document, and be shared by all color space /// needed) in the final document, and be shared by all color space
/// dictionaries. /// dictionaries.
pub struct ColorFunctionRefs { pub struct ColorFunctionRefs {
oklab: Option<Ref>,
srgb: Option<Ref>, srgb: Option<Ref>,
d65_gray: Option<Ref>, d65_gray: Option<Ref>,
} }
impl Renumber for ColorFunctionRefs { impl Renumber for ColorFunctionRefs {
fn renumber(&mut self, offset: i32) { fn renumber(&mut self, offset: i32) {
if let Some(r) = &mut self.oklab {
r.renumber(offset);
}
if let Some(r) = &mut self.srgb { if let Some(r) = &mut self.srgb {
r.renumber(offset); r.renumber(offset);
} }
@ -183,7 +151,6 @@ pub fn alloc_color_functions_refs(
}); });
let refs = ColorFunctionRefs { let refs = ColorFunctionRefs {
oklab: if used_color_spaces.use_oklab { Some(chunk.alloc()) } else { None },
srgb: if used_color_spaces.use_srgb { Some(chunk.alloc()) } else { None }, srgb: if used_color_spaces.use_srgb { Some(chunk.alloc()) } else { None },
d65_gray: if used_color_spaces.use_d65_gray { Some(chunk.alloc()) } else { None }, d65_gray: if used_color_spaces.use_d65_gray { Some(chunk.alloc()) } else { None },
}; };
@ -191,28 +158,6 @@ pub fn alloc_color_functions_refs(
(chunk, refs) (chunk, refs)
} }
/// This function removes comments, line spaces and carriage returns from a
/// PostScript program. This is necessary to optimize the size of the PDF file.
fn minify(source: &str) -> String {
let mut buf = String::with_capacity(source.len());
let mut s = unscanny::Scanner::new(source);
while let Some(c) = s.eat() {
match c {
'%' => {
s.eat_until('\n');
}
c if c.is_whitespace() => {
s.eat_whitespace();
if buf.ends_with(|c: char| !c.is_whitespace()) {
buf.push(' ');
}
}
_ => buf.push(c),
}
}
buf
}
/// Encodes the color into four f32s, which can be used in a PDF file. /// Encodes the color into four f32s, which can be used in a PDF file.
/// Ensures that the values are in the range [0.0, 1.0]. /// Ensures that the values are in the range [0.0, 1.0].
/// ///
@ -233,13 +178,7 @@ impl ColorEncode for ColorSpace {
fn encode(&self, color: Color) -> [f32; 4] { fn encode(&self, color: Color) -> [f32; 4] {
match self { match self {
ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => { ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => {
let [l, c, h, alpha] = color.to_oklch().to_vec4(); color.to_space(ColorSpace::Srgb).to_vec4()
// Clamp on Oklch's chroma, not Oklab's a\* and b\* as to not distort hue.
let c = c.clamp(0.0, 0.5);
// Convert cylindrical coordinates back to rectangular ones.
let a = c * h.to_radians().cos();
let b = c * h.to_radians().sin();
[l, a + 0.5, b + 0.5, alpha]
} }
_ => color.to_space(*self).to_vec4(), _ => color.to_space(*self).to_vec4(),
} }
@ -303,14 +242,6 @@ impl PaintEncode for Color {
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
ctx.content.set_fill_color([l]); ctx.content.set_fill_color([l]);
} }
// Oklch is converted to Oklab.
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Oklab);
ctx.set_fill_color_space(OKLAB);
let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
ctx.content.set_fill_color([l, a, b]);
}
Color::LinearRgb(_) => { Color::LinearRgb(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_fill_color_space(LINEAR_SRGB); ctx.set_fill_color_space(LINEAR_SRGB);
@ -318,7 +249,12 @@ impl PaintEncode for Color {
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_fill_color([r, g, b]); ctx.content.set_fill_color([r, g, b]);
} }
Color::Rgb(_) => { // Oklab & friends are encoded as RGB.
Color::Rgb(_)
| Color::Oklab(_)
| Color::Oklch(_)
| Color::Hsl(_)
| Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Srgb); ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_fill_color_space(SRGB); ctx.set_fill_color_space(SRGB);
@ -343,14 +279,6 @@ impl PaintEncode for Color {
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
ctx.content.set_stroke_color([l]); ctx.content.set_stroke_color([l]);
} }
// Oklch is converted to Oklab.
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Oklab);
ctx.set_stroke_color_space(OKLAB);
let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
ctx.content.set_stroke_color([l, a, b]);
}
Color::LinearRgb(_) => { Color::LinearRgb(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_stroke_color_space(LINEAR_SRGB); ctx.set_stroke_color_space(LINEAR_SRGB);
@ -358,7 +286,12 @@ impl PaintEncode for Color {
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_stroke_color([r, g, b]); ctx.content.set_stroke_color([r, g, b]);
} }
Color::Rgb(_) => { // Oklab & friends are encoded as RGB.
Color::Rgb(_)
| Color::Oklab(_)
| Color::Oklch(_)
| Color::Hsl(_)
| Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Srgb); ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_stroke_color_space(SRGB); ctx.set_stroke_color_space(SRGB);

View File

@ -181,9 +181,9 @@ fn shading_function(
for window in gradient.stops_ref().windows(2) { for window in gradient.stops_ref().windows(2) {
let (first, second) = (window[0], window[1]); let (first, second) = (window[0], window[1]);
// If we have a hue index, we will create several stops in-between // If we have a hue index or are using Oklab, we will create several
// to make the gradient smoother without interpolation issues with // stops in-between to make the gradient smoother without interpolation
// native color spaces. // issues with native color spaces.
let mut last_c = first.0; let mut last_c = first.0;
if gradient.space().hue_index().is_some() { if gradient.space().hue_index().is_some() {
for i in 0..=32 { for i in 0..=32 {

View File

@ -1,78 +0,0 @@
{
% Starting stack: L, A, B
% /!\ WARNING: The A and B components **MUST** be encoded
% in the range [0, 1] before calling this function.
% This is because the function assumes that the
% A and B components are offset by a factor of 0.5
% in order to meet the range requirements of the
% PDF specification.
exch 0.5 sub
exch 0.5 sub
% Load L a and b into the stack
2 index
2 index
2 index
% Compute f1 = ((0.3963377774 * a) + (0.2158037573 * b) + L)^3
0.2158037573 mul exch
0.3963377774 mul add add
dup dup mul mul
% Load L, a, and b into the stack
3 index
3 index
3 index
% Compute f2 = ((-0.1055613458 * a) + (-0.0638541728 * b) + L)^3
-0.0638541728 mul exch
-0.1055613458 mul add add
dup dup mul mul
% Load L, a, and b into the stack
4 index
4 index
4 index
% Compute f3 = ((-0.0894841775 * a) + (-1.2914855480 * b) + L)^3
-1.2914855480 mul exch
-0.0894841775 mul add add
dup dup mul mul
% Discard L, a, and b by rolling the stack and popping
6 3 roll pop pop pop
% Load f1, f2, and f3 into the stack
2 index
2 index
2 index
% Compute R = f1 * 4.0767416621 + f2 * -3.3077115913 + f3 * 0.2309699292
0.2309699292 mul exch
-3.3077115913 mul add exch
4.0767416621 mul add
% Load f1, f2, and f3 into the stack
3 index
3 index
3 index
% Compute G = f1 * -1.2684380046 + f2 * 2.6097574011 + f3 * -0.3413193965
-0.3413193965 mul exch
2.6097574011 mul add exch
-1.2684380046 mul add
% Load f1, f2, and f3 into the stack
4 index
4 index
4 index
% Compute B = f1 * -0.0041960863 + f2 * -0.7034186147 + f3 * 1.7076147010
1.7076147010 mul exch
-0.7034186147 mul add exch
-0.0041960863 mul add
% Discard f1, f2, and f3 by rolling the stack and popping
6 3 roll pop pop pop
}

View File

@ -169,11 +169,12 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// consider the following: /// consider the following:
/// - SVG gradients are currently inefficiently encoded. This will be improved /// - SVG gradients are currently inefficiently encoded. This will be improved
/// in the future. /// in the future.
/// - PDF gradients in the [`color.hsv`]($color.hsv), [`color.hsl`]($color.hsl), /// - PDF gradients in the [`color.oklab`]($color.oklab), [`color.hsv`]($color.hsv),
/// and [`color.oklch`]($color.oklch) color spaces are stored as a list of /// [`color.hsl`]($color.hsl), and [`color.oklch`]($color.oklch) color spaces
/// [`color.oklab`]($color.oklab) colors with extra stops in between. This /// are stored as a list of [`color.rgb`]($color.rgb) colors with extra stops
/// avoids needing to encode these color spaces in your PDF file, but it does /// in between. This avoids needing to encode these color spaces in your PDF
/// add extra stops to your gradient, which can increase the file size. /// file, but it does add extra stops to your gradient, which can increase
/// the file size.
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash)]
pub enum Gradient { pub enum Gradient {