mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Remove HSV and HSL color spaces from PDF export (#2927)
Co-authored-by: EpicEricEE <github@ericbiedert.de>
This commit is contained in:
parent
077d6b5c54
commit
d869a07d2d
@ -10,20 +10,12 @@ use crate::page::{PageContext, Transforms};
|
|||||||
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 OKLAB: Name<'static> = Name(b"oklab");
|
||||||
pub const HSV: Name<'static> = Name(b"hsv");
|
|
||||||
pub const HSL: Name<'static> = Name(b"hsl");
|
|
||||||
pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb");
|
pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb");
|
||||||
|
|
||||||
// The names of the color components.
|
// The names of the color components.
|
||||||
const OKLAB_L: Name<'static> = Name(b"L");
|
const OKLAB_L: Name<'static> = Name(b"L");
|
||||||
const OKLAB_A: Name<'static> = Name(b"A");
|
const OKLAB_A: Name<'static> = Name(b"A");
|
||||||
const OKLAB_B: Name<'static> = Name(b"B");
|
const OKLAB_B: Name<'static> = Name(b"B");
|
||||||
const HSV_H: Name<'static> = Name(b"H");
|
|
||||||
const HSV_S: Name<'static> = Name(b"S");
|
|
||||||
const HSV_V: Name<'static> = Name(b"V");
|
|
||||||
const HSL_H: Name<'static> = Name(b"H");
|
|
||||||
const HSL_S: Name<'static> = Name(b"S");
|
|
||||||
const HSL_L: Name<'static> = Name(b"L");
|
|
||||||
|
|
||||||
// The ICC profiles.
|
// The ICC profiles.
|
||||||
static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> =
|
static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> =
|
||||||
@ -34,10 +26,6 @@ static GRAY_ICC_DEFLATED: Lazy<Vec<u8>> =
|
|||||||
// The PostScript functions for color spaces.
|
// The PostScript functions for color spaces.
|
||||||
static OKLAB_DEFLATED: Lazy<Vec<u8>> =
|
static OKLAB_DEFLATED: Lazy<Vec<u8>> =
|
||||||
Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes()));
|
Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes()));
|
||||||
static HSV_DEFLATED: Lazy<Vec<u8>> =
|
|
||||||
Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes()));
|
|
||||||
static HSL_DEFLATED: Lazy<Vec<u8>> =
|
|
||||||
Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes()));
|
|
||||||
|
|
||||||
/// The color spaces present in the PDF document
|
/// The color spaces present in the PDF document
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -45,8 +33,6 @@ pub struct ColorSpaces {
|
|||||||
oklab: Option<Ref>,
|
oklab: Option<Ref>,
|
||||||
srgb: Option<Ref>,
|
srgb: Option<Ref>,
|
||||||
d65_gray: Option<Ref>,
|
d65_gray: Option<Ref>,
|
||||||
hsv: Option<Ref>,
|
|
||||||
hsl: Option<Ref>,
|
|
||||||
use_linear_rgb: bool,
|
use_linear_rgb: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,24 +56,6 @@ impl ColorSpaces {
|
|||||||
*self.d65_gray.get_or_insert_with(|| alloc.bump())
|
*self.d65_gray.get_or_insert_with(|| alloc.bump())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the hsv color space.
|
|
||||||
///
|
|
||||||
/// # Warning
|
|
||||||
/// The Hue component of the color must be in degrees and must be divided
|
|
||||||
/// by 360.0 before being encoded into the PDF file.
|
|
||||||
pub fn hsv(&mut self, alloc: &mut Ref) -> Ref {
|
|
||||||
*self.hsv.get_or_insert_with(|| alloc.bump())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a reference to the hsl color space.
|
|
||||||
///
|
|
||||||
/// # Warning
|
|
||||||
/// The Hue component of the color must be in degrees and must be divided
|
|
||||||
/// by 360.0 before being encoded into the PDF file.
|
|
||||||
pub fn hsl(&mut self, alloc: &mut Ref) -> Ref {
|
|
||||||
*self.hsl.get_or_insert_with(|| alloc.bump())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark linear RGB as used.
|
/// Mark linear RGB as used.
|
||||||
pub fn linear_rgb(&mut self) {
|
pub fn linear_rgb(&mut self) {
|
||||||
self.use_linear_rgb = true;
|
self.use_linear_rgb = true;
|
||||||
@ -101,7 +69,7 @@ impl ColorSpaces {
|
|||||||
alloc: &mut Ref,
|
alloc: &mut Ref,
|
||||||
) {
|
) {
|
||||||
match color_space {
|
match color_space {
|
||||||
ColorSpace::Oklab => {
|
ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => {
|
||||||
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]);
|
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]);
|
||||||
self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc);
|
self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc);
|
||||||
oklab.tint_ref(self.oklab(alloc));
|
oklab.tint_ref(self.oklab(alloc));
|
||||||
@ -121,18 +89,6 @@ impl ColorSpaces {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ColorSpace::Hsl => {
|
|
||||||
let mut hsl = writer.device_n([HSL_H, HSL_S, HSL_L]);
|
|
||||||
self.write(ColorSpace::Srgb, hsl.alternate_color_space(), alloc);
|
|
||||||
hsl.tint_ref(self.hsl(alloc));
|
|
||||||
hsl.attrs().subtype(DeviceNSubtype::DeviceN);
|
|
||||||
}
|
|
||||||
ColorSpace::Hsv => {
|
|
||||||
let mut hsv = writer.device_n([HSV_H, HSV_S, HSV_V]);
|
|
||||||
self.write(ColorSpace::Srgb, hsv.alternate_color_space(), alloc);
|
|
||||||
hsv.tint_ref(self.hsv(alloc));
|
|
||||||
hsv.attrs().subtype(DeviceNSubtype::DeviceN);
|
|
||||||
}
|
|
||||||
ColorSpace::Cmyk => writer.device_cmyk(),
|
ColorSpace::Cmyk => writer.device_cmyk(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -151,14 +107,6 @@ impl ColorSpaces {
|
|||||||
self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc);
|
self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hsv.is_some() {
|
|
||||||
self.write(ColorSpace::Hsv, spaces.insert(HSV).start(), alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.hsl.is_some() {
|
|
||||||
self.write(ColorSpace::Hsl, spaces.insert(HSL).start(), alloc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.use_linear_rgb {
|
if self.use_linear_rgb {
|
||||||
self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc);
|
self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc);
|
||||||
}
|
}
|
||||||
@ -176,24 +124,6 @@ impl ColorSpaces {
|
|||||||
.filter(Filter::FlateDecode);
|
.filter(Filter::FlateDecode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the HSV function & color space.
|
|
||||||
if let Some(hsv) = self.hsv {
|
|
||||||
chunk
|
|
||||||
.post_script_function(hsv, &HSV_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 HSL function & color space.
|
|
||||||
if let Some(hsl) = self.hsl {
|
|
||||||
chunk
|
|
||||||
.post_script_function(hsl, &HSL_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 let Some(srgb) = self.srgb {
|
if let Some(srgb) = self.srgb {
|
||||||
chunk
|
chunk
|
||||||
@ -255,7 +185,7 @@ pub trait ColorEncode {
|
|||||||
impl ColorEncode for ColorSpace {
|
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::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => {
|
||||||
let [l, c, h, alpha] = color.to_oklch().to_vec4();
|
let [l, c, h, alpha] = color.to_oklch().to_vec4();
|
||||||
// Clamp on Oklch's chroma, not Oklab's a\* and b\* as to not distort hue.
|
// Clamp on Oklch's chroma, not Oklab's a\* and b\* as to not distort hue.
|
||||||
let c = c.clamp(0.0, 0.5);
|
let c = c.clamp(0.0, 0.5);
|
||||||
@ -264,15 +194,7 @@ impl ColorEncode for ColorSpace {
|
|||||||
let b = c * h.to_radians().sin();
|
let b = c * h.to_radians().sin();
|
||||||
[l, a + 0.5, b + 0.5, alpha]
|
[l, a + 0.5, b + 0.5, alpha]
|
||||||
}
|
}
|
||||||
ColorSpace::Hsl => {
|
_ => color.to_space(*self).to_vec4(),
|
||||||
let [h, s, l, _] = color.to_hsl().to_vec4();
|
|
||||||
[h / 360.0, s, l, 0.0]
|
|
||||||
}
|
|
||||||
ColorSpace::Hsv => {
|
|
||||||
let [h, s, v, _] = color.to_hsv().to_vec4();
|
|
||||||
[h / 360.0, s, v, 0.0]
|
|
||||||
}
|
|
||||||
_ => color.to_vec4(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,7 +237,7 @@ impl PaintEncode for Color {
|
|||||||
ctx.content.set_fill_color([l]);
|
ctx.content.set_fill_color([l]);
|
||||||
}
|
}
|
||||||
// Oklch is converted to Oklab.
|
// Oklch is converted to Oklab.
|
||||||
Color::Oklab(_) | Color::Oklch(_) => {
|
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
|
||||||
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
||||||
ctx.set_fill_color_space(OKLAB);
|
ctx.set_fill_color_space(OKLAB);
|
||||||
|
|
||||||
@ -342,20 +264,6 @@ impl PaintEncode for Color {
|
|||||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
||||||
ctx.content.set_fill_cmyk(c, m, y, k);
|
ctx.content.set_fill_cmyk(c, m, y, k);
|
||||||
}
|
}
|
||||||
Color::Hsl(_) => {
|
|
||||||
ctx.parent.colors.hsl(&mut ctx.parent.alloc);
|
|
||||||
ctx.set_fill_color_space(HSL);
|
|
||||||
|
|
||||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*self);
|
|
||||||
ctx.content.set_fill_color([h, s, l]);
|
|
||||||
}
|
|
||||||
Color::Hsv(_) => {
|
|
||||||
ctx.parent.colors.hsv(&mut ctx.parent.alloc);
|
|
||||||
ctx.set_fill_color_space(HSV);
|
|
||||||
|
|
||||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*self);
|
|
||||||
ctx.content.set_fill_color([h, s, v]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,7 +277,7 @@ impl PaintEncode for Color {
|
|||||||
ctx.content.set_stroke_color([l]);
|
ctx.content.set_stroke_color([l]);
|
||||||
}
|
}
|
||||||
// Oklch is converted to Oklab.
|
// Oklch is converted to Oklab.
|
||||||
Color::Oklab(_) | Color::Oklch(_) => {
|
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
|
||||||
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
ctx.parent.colors.oklab(&mut ctx.parent.alloc);
|
||||||
ctx.set_stroke_color_space(OKLAB);
|
ctx.set_stroke_color_space(OKLAB);
|
||||||
|
|
||||||
@ -396,20 +304,6 @@ impl PaintEncode for Color {
|
|||||||
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
|
||||||
ctx.content.set_stroke_cmyk(c, m, y, k);
|
ctx.content.set_stroke_cmyk(c, m, y, k);
|
||||||
}
|
}
|
||||||
Color::Hsl(_) => {
|
|
||||||
ctx.parent.colors.hsl(&mut ctx.parent.alloc);
|
|
||||||
ctx.set_stroke_color_space(HSL);
|
|
||||||
|
|
||||||
let [h, s, l, _] = ColorSpace::Hsl.encode(*self);
|
|
||||||
ctx.content.set_stroke_color([h, s, l]);
|
|
||||||
}
|
|
||||||
Color::Hsv(_) => {
|
|
||||||
ctx.parent.colors.hsv(&mut ctx.parent.alloc);
|
|
||||||
ctx.set_stroke_color_space(HSV);
|
|
||||||
|
|
||||||
let [h, s, v, _] = ColorSpace::Hsv.encode(*self);
|
|
||||||
ctx.content.set_stroke_color([h, s, v]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use pdf_writer::{Filter, Finish, Name, Ref};
|
|||||||
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
|
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
|
||||||
use typst::util::Numeric;
|
use typst::util::Numeric;
|
||||||
use typst::visualize::{
|
use typst::visualize::{
|
||||||
Color, ColorSpace, ConicGradient, Gradient, RelativeTo, WeightedColor,
|
Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
|
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor};
|
||||||
@ -49,7 +49,13 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
ctx.colors
|
ctx.colors
|
||||||
.write(gradient.space(), shading.color_space(), &mut ctx.alloc);
|
.write(gradient.space(), shading.color_space(), &mut ctx.alloc);
|
||||||
|
|
||||||
let (sin, cos) = (angle.sin(), angle.cos());
|
let (mut sin, mut cos) = (angle.sin(), angle.cos());
|
||||||
|
|
||||||
|
// Scale to edges of unit square.
|
||||||
|
let factor = cos.abs() + sin.abs();
|
||||||
|
sin *= factor;
|
||||||
|
cos *= factor;
|
||||||
|
|
||||||
let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() {
|
let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() {
|
||||||
Quadrant::First => (0.0, 0.0, cos, sin),
|
Quadrant::First => (0.0, 0.0, cos, sin),
|
||||||
Quadrant::Second => (1.0, 0.0, cos + 1.0, sin),
|
Quadrant::Second => (1.0, 0.0, cos + 1.0, sin),
|
||||||
@ -57,12 +63,6 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0),
|
Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0),
|
||||||
};
|
};
|
||||||
|
|
||||||
let clamp = |i: f64| if i < 1e-4 { 0.0 } else { i.clamp(0.0, 1.0) };
|
|
||||||
let x1 = clamp(x1);
|
|
||||||
let y1 = clamp(y1);
|
|
||||||
let x2 = clamp(x2);
|
|
||||||
let y2 = clamp(y2);
|
|
||||||
|
|
||||||
shading
|
shading
|
||||||
.anti_alias(gradient.anti_alias())
|
.anti_alias(gradient.anti_alias())
|
||||||
.function(shading_function)
|
.function(shading_function)
|
||||||
@ -100,7 +100,7 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
shading_pattern
|
shading_pattern
|
||||||
}
|
}
|
||||||
Gradient::Conic(conic) => {
|
Gradient::Conic(conic) => {
|
||||||
let vertices = compute_vertex_stream(conic, aspect_ratio);
|
let vertices = compute_vertex_stream(&gradient, aspect_ratio);
|
||||||
|
|
||||||
let stream_shading_id = ctx.alloc.bump();
|
let stream_shading_id = ctx.alloc.bump();
|
||||||
let mut stream_shading =
|
let mut stream_shading =
|
||||||
@ -148,73 +148,20 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
|
|||||||
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]);
|
||||||
|
|
||||||
// Skip stops with the same position.
|
// If we have a hue index, we will create several stops in-between
|
||||||
if first.1.get() == second.1.get() {
|
// to make the gradient smoother without interpolation issues with
|
||||||
continue;
|
// native color spaces.
|
||||||
}
|
let mut last_c = first.0;
|
||||||
|
if gradient.space().hue_index().is_some() {
|
||||||
|
for i in 0..=32 {
|
||||||
|
let t = i as f64 / 32.0;
|
||||||
|
let real_t = first.1.get() * (1.0 - t) + second.1.get() * t;
|
||||||
|
|
||||||
// If the color space is HSL or HSV, and we cross the 0°/360° boundary,
|
let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t)));
|
||||||
// we need to create two separate stops.
|
functions.push(single_gradient(ctx, last_c, c, ColorSpace::Oklab));
|
||||||
if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv {
|
bounds.push(real_t as f32);
|
||||||
let t1 = first.1.get() as f32;
|
encode.extend([0.0, 1.0]);
|
||||||
let t2 = second.1.get() as f32;
|
last_c = c;
|
||||||
let [h1, s1, x1, _] = first.0.to_space(gradient.space()).to_vec4();
|
|
||||||
let [h2, s2, x2, _] = second.0.to_space(gradient.space()).to_vec4();
|
|
||||||
|
|
||||||
// Compute the intermediary stop at 360°.
|
|
||||||
if (h1 - h2).abs() > 180.0 {
|
|
||||||
let h1 = if h1 < h2 { h1 + 360.0 } else { h1 };
|
|
||||||
let h2 = if h2 < h1 { h2 + 360.0 } else { h2 };
|
|
||||||
|
|
||||||
// We compute where the crossing happens between zero and one
|
|
||||||
let t = (360.0 - h1) / (h2 - h1);
|
|
||||||
// We then map it back to the original range.
|
|
||||||
let t_prime = t * (t2 - t1) + t1;
|
|
||||||
|
|
||||||
// If the crossing happens between the two stops,
|
|
||||||
// we need to create an extra stop.
|
|
||||||
if t_prime <= t2 && t_prime >= t1 {
|
|
||||||
bounds.push(t_prime);
|
|
||||||
bounds.push(t_prime);
|
|
||||||
bounds.push(t2);
|
|
||||||
encode.extend([0.0, 1.0]);
|
|
||||||
encode.extend([0.0, 1.0]);
|
|
||||||
encode.extend([0.0, 1.0]);
|
|
||||||
|
|
||||||
// These need to be individual function to encode 360.0 correctly.
|
|
||||||
let func1 = ctx.alloc.bump();
|
|
||||||
ctx.pdf
|
|
||||||
.exponential_function(func1)
|
|
||||||
.range(gradient.space().range())
|
|
||||||
.c0(gradient.space().convert(first.0))
|
|
||||||
.c1([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
|
||||||
.domain([0.0, 1.0])
|
|
||||||
.n(1.0);
|
|
||||||
|
|
||||||
let func2 = ctx.alloc.bump();
|
|
||||||
ctx.pdf
|
|
||||||
.exponential_function(func2)
|
|
||||||
.range(gradient.space().range())
|
|
||||||
.c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
|
||||||
.c1([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
|
||||||
.domain([0.0, 1.0])
|
|
||||||
.n(1.0);
|
|
||||||
|
|
||||||
let func3 = ctx.alloc.bump();
|
|
||||||
ctx.pdf
|
|
||||||
.exponential_function(func3)
|
|
||||||
.range(gradient.space().range())
|
|
||||||
.c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
|
|
||||||
.c1(gradient.space().convert(second.0))
|
|
||||||
.domain([0.0, 1.0])
|
|
||||||
.n(1.0);
|
|
||||||
|
|
||||||
functions.push(func1);
|
|
||||||
functions.push(func2);
|
|
||||||
functions.push(func3);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,108 +374,76 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point,
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[comemo::memoize]
|
#[comemo::memoize]
|
||||||
fn compute_vertex_stream(conic: &ConicGradient, aspect_ratio: Ratio) -> Arc<Vec<u8>> {
|
fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc<Vec<u8>> {
|
||||||
|
let Gradient::Conic(conic) = gradient else { unreachable!() };
|
||||||
|
|
||||||
// Generated vertices for the Coons patches
|
// Generated vertices for the Coons patches
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
|
|
||||||
// Correct the gradient's angle
|
// Correct the gradient's angle
|
||||||
let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio);
|
let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio);
|
||||||
|
|
||||||
// We want to generate a vertex based on some conditions, either:
|
|
||||||
// - At the boundary of a stop
|
|
||||||
// - At the boundary of a quadrant
|
|
||||||
// - When we cross the boundary of a hue turn (for HSV and HSL only)
|
|
||||||
for window in conic.stops.windows(2) {
|
for window in conic.stops.windows(2) {
|
||||||
let ((c0, t0), (c1, t1)) = (window[0], window[1]);
|
let ((c0, t0), (c1, t1)) = (window[0], window[1]);
|
||||||
|
|
||||||
// Skip stops with the same position
|
// Precision:
|
||||||
|
// - On an even color, insert a stop every 90deg
|
||||||
|
// - For a hue-based color space, insert 200 stops minimum
|
||||||
|
// - On any other, insert 20 stops minimum
|
||||||
|
let max_dt = if c0 == c1 {
|
||||||
|
0.25
|
||||||
|
} else if conic.space.hue_index().is_some() {
|
||||||
|
0.005
|
||||||
|
} else {
|
||||||
|
0.05
|
||||||
|
};
|
||||||
|
let encode_space = conic
|
||||||
|
.space
|
||||||
|
.hue_index()
|
||||||
|
.map(|_| ColorSpace::Oklab)
|
||||||
|
.unwrap_or(conic.space);
|
||||||
|
let mut t_x = t0.get();
|
||||||
|
let dt = (t1.get() - t0.get()).min(max_dt);
|
||||||
|
|
||||||
|
// Special casing for sharp gradients.
|
||||||
if t0 == t1 {
|
if t0 == t1 {
|
||||||
|
write_patch(
|
||||||
|
&mut vertices,
|
||||||
|
t0.get() as f32,
|
||||||
|
t1.get() as f32,
|
||||||
|
encode_space.convert(c0),
|
||||||
|
encode_space.convert(c1),
|
||||||
|
angle,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the angle between the two stops is greater than 90 degrees, we need to
|
|
||||||
// generate a vertex at the boundary of the quadrant.
|
|
||||||
// However, we add more stops in-between to make the gradient smoother, so we
|
|
||||||
// need to generate a vertex at least every 5 degrees.
|
|
||||||
// If the colors are the same, we do it every quadrant only.
|
|
||||||
let slope = 1.0 / (t1.get() - t0.get());
|
|
||||||
let mut t_x = t0.get();
|
|
||||||
let dt = (t1.get() - t0.get()).min(0.25);
|
|
||||||
while t_x < t1.get() {
|
while t_x < t1.get() {
|
||||||
let t_next = (t_x + dt).min(t1.get());
|
let t_next = (t_x + dt).min(t1.get());
|
||||||
|
|
||||||
let t1 = slope * (t_x - t0.get());
|
// The current progress in the current window.
|
||||||
let t2 = slope * (t_next - t0.get());
|
let t = |t| (t - t0.get()) / (t1.get() - t0.get());
|
||||||
|
|
||||||
// We don't use `Gradient::sample` to avoid issues with sharp gradients.
|
|
||||||
let c = Color::mix_iter(
|
let c = Color::mix_iter(
|
||||||
[WeightedColor::new(c0, 1.0 - t1), WeightedColor::new(c1, t1)],
|
[WeightedColor::new(c0, 1.0 - t(t_x)), WeightedColor::new(c1, t(t_x))],
|
||||||
conic.space,
|
conic.space,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let c_next = Color::mix_iter(
|
let c_next = Color::mix_iter(
|
||||||
[WeightedColor::new(c0, 1.0 - t2), WeightedColor::new(c1, t2)],
|
[
|
||||||
|
WeightedColor::new(c0, 1.0 - t(t_next)),
|
||||||
|
WeightedColor::new(c1, t(t_next)),
|
||||||
|
],
|
||||||
conic.space,
|
conic.space,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// If the color space is HSL or HSV, and we cross the 0°/360° boundary,
|
|
||||||
// we need to create two separate stops.
|
|
||||||
if conic.space == ColorSpace::Hsl || conic.space == ColorSpace::Hsv {
|
|
||||||
let [h1, s1, x1, _] = c.to_space(conic.space).to_vec4();
|
|
||||||
let [h2, s2, x2, _] = c_next.to_space(conic.space).to_vec4();
|
|
||||||
|
|
||||||
// Compute the intermediary stop at 360°.
|
|
||||||
if (h1 - h2).abs() > 180.0 {
|
|
||||||
let h1 = if h1 < h2 { h1 + 360.0 } else { h1 };
|
|
||||||
let h2 = if h2 < h1 { h2 + 360.0 } else { h2 };
|
|
||||||
|
|
||||||
// We compute where the crossing happens between zero and one
|
|
||||||
let t = (360.0 - h1) / (h2 - h1);
|
|
||||||
// We then map it back to the original range.
|
|
||||||
let t_prime = t * (t_next as f32 - t_x as f32) + t_x as f32;
|
|
||||||
|
|
||||||
// If the crossing happens between the two stops,
|
|
||||||
// we need to create an extra stop.
|
|
||||||
if t_prime <= t_next as f32 && t_prime >= t_x as f32 {
|
|
||||||
let c0 = [1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t];
|
|
||||||
let c1 = [0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t];
|
|
||||||
let c0 = c0.map(|c| u16::quantize(c, [0.0, 1.0]));
|
|
||||||
let c1 = c1.map(|c| u16::quantize(c, [0.0, 1.0]));
|
|
||||||
|
|
||||||
write_patch(
|
|
||||||
&mut vertices,
|
|
||||||
t_x as f32,
|
|
||||||
t_prime,
|
|
||||||
conic.space.convert(c),
|
|
||||||
c0,
|
|
||||||
angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
write_patch(&mut vertices, t_prime, t_prime, c0, c1, angle);
|
|
||||||
|
|
||||||
write_patch(
|
|
||||||
&mut vertices,
|
|
||||||
t_prime,
|
|
||||||
t_next as f32,
|
|
||||||
c1,
|
|
||||||
conic.space.convert(c_next),
|
|
||||||
angle,
|
|
||||||
);
|
|
||||||
|
|
||||||
t_x = t_next;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_patch(
|
write_patch(
|
||||||
&mut vertices,
|
&mut vertices,
|
||||||
t_x as f32,
|
t_x as f32,
|
||||||
t_next as f32,
|
t_next as f32,
|
||||||
conic.space.convert(c),
|
encode_space.convert(c),
|
||||||
conic.space.convert(c_next),
|
encode_space.convert(c_next),
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
% Starting stack: H, S, L
|
|
||||||
% /!\ WARNING: The hue component **MUST** be encoded
|
|
||||||
% in the range [0, 1] before calling this function.
|
|
||||||
% This is because the function assumes that the
|
|
||||||
% hue component are divided by a factor of 360
|
|
||||||
% in order to meet the range requirements of the
|
|
||||||
% PDF specification.
|
|
||||||
|
|
||||||
% First we do H = (H * 360.0) % 360
|
|
||||||
3 2 roll 360 mul 3 1 roll
|
|
||||||
|
|
||||||
% Compute C = (1 - |2 * L - 1|) * S
|
|
||||||
dup 1 exch 2 mul 1 sub abs sub 3 2 roll mul
|
|
||||||
|
|
||||||
% P = (H / 60) % 2
|
|
||||||
3 2 roll dup 60 div 2
|
|
||||||
2 copy div cvi mul exch sub abs
|
|
||||||
|
|
||||||
% X = C * (1 - |P - 1|)
|
|
||||||
1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul
|
|
||||||
|
|
||||||
% Compute m = L - C / 2
|
|
||||||
exch dup 2 div 5 4 roll exch sub
|
|
||||||
|
|
||||||
% Rotate so H is top
|
|
||||||
4 3 roll exch 4 1 roll
|
|
||||||
|
|
||||||
% Construct the RGB stack
|
|
||||||
dup 60 lt {
|
|
||||||
% We need to build: (C, X, 0)
|
|
||||||
pop 0 3 1 roll
|
|
||||||
} {
|
|
||||||
dup 120 lt {
|
|
||||||
% We need to build: (X, C, 0)
|
|
||||||
pop exch 0 3 1 roll
|
|
||||||
} {
|
|
||||||
dup 180 lt {
|
|
||||||
% We need to build: (0, C, X)
|
|
||||||
pop 0
|
|
||||||
} {
|
|
||||||
dup 240 lt {
|
|
||||||
% We need to build: (0, X, C)
|
|
||||||
pop exch 0
|
|
||||||
} {
|
|
||||||
300 lt {
|
|
||||||
% We need to build: (X, 0, C)
|
|
||||||
0 3 2 roll
|
|
||||||
} {
|
|
||||||
% We need to build: (C, 0, X)
|
|
||||||
0 exch
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
|
|
||||||
4 3 roll
|
|
||||||
|
|
||||||
% Add m to each component
|
|
||||||
dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
% Starting stack: H, S, V
|
|
||||||
% /!\ WARNING: The hue component **MUST** be encoded
|
|
||||||
% in the range [0, 1] before calling this function.
|
|
||||||
% This is because the function assumes that the
|
|
||||||
% hue component are divided by a factor of 360
|
|
||||||
% in order to meet the range requirements of the
|
|
||||||
% PDF specification.
|
|
||||||
|
|
||||||
% First we do H = (H * 360.0) % 360
|
|
||||||
3 2 roll 360 mul 3 1 roll
|
|
||||||
|
|
||||||
% Compute C = V * S
|
|
||||||
dup 3 1 roll mul
|
|
||||||
|
|
||||||
% P = (H / 60) % 2
|
|
||||||
3 2 roll dup 60 div 2
|
|
||||||
2 copy div cvi mul exch sub abs
|
|
||||||
|
|
||||||
% X = C * (1 - |P - 1|)
|
|
||||||
1 exch 1 sub abs sub 3 2 roll dup 3 1 roll mul
|
|
||||||
|
|
||||||
% Compute m = V - C
|
|
||||||
exch dup 5 4 roll exch sub
|
|
||||||
|
|
||||||
% Rotate so H is top
|
|
||||||
4 3 roll exch 4 1 roll
|
|
||||||
|
|
||||||
% Construct the RGB stack
|
|
||||||
dup 60 lt {
|
|
||||||
% We need to build: (C, X, 0)
|
|
||||||
pop 0 3 1 roll
|
|
||||||
} {
|
|
||||||
dup 120 lt {
|
|
||||||
% We need to build: (X, C, 0)
|
|
||||||
pop exch 0 3 1 roll
|
|
||||||
} {
|
|
||||||
dup 180 lt {
|
|
||||||
% We need to build: (0, C, X)
|
|
||||||
pop 0
|
|
||||||
} {
|
|
||||||
dup 240 lt {
|
|
||||||
% We need to build: (0, X, C)
|
|
||||||
pop exch 0
|
|
||||||
} {
|
|
||||||
300 lt {
|
|
||||||
% We need to build: (X, 0, C)
|
|
||||||
0 3 2 roll
|
|
||||||
} {
|
|
||||||
% We need to build: (C, 0, X)
|
|
||||||
0 exch
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
|
|
||||||
4 3 roll
|
|
||||||
|
|
||||||
% Add m to each component
|
|
||||||
dup dup 6 2 roll add 5 2 roll add exch 4 3 roll add exch
|
|
||||||
}
|
|
@ -161,6 +161,20 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
|
|||||||
/// # Presets
|
/// # Presets
|
||||||
/// Typst predefines color maps that you can use with your gradients. See the
|
/// Typst predefines color maps that you can use with your gradients. See the
|
||||||
/// [`color`]($color/#predefined-color-maps) documentation for more details.
|
/// [`color`]($color/#predefined-color-maps) documentation for more details.
|
||||||
|
///
|
||||||
|
/// # Note on file sizes
|
||||||
|
///
|
||||||
|
/// Gradients can be quite large, especially if they have many stops. This is
|
||||||
|
/// because gradients are stored as a list of colors and offsets, which can
|
||||||
|
/// take up a lot of space. If you are concerned about file sizes, you should
|
||||||
|
/// consider the following:
|
||||||
|
/// - SVG gradients are currently inefficiently encoded. This will be improved
|
||||||
|
/// in the future.
|
||||||
|
/// - PDF gradients in the [`color.hsv`]($color.hsv), [`color.hsl`]($color.hsl),
|
||||||
|
/// and [`color.oklch`]($color.oklch) color spaces are stored as a list of
|
||||||
|
/// [`color.oklab`]($color.oklab) colors with extra stops in between. This
|
||||||
|
/// avoids needing to encode these color spaces in your PDF file, but it does
|
||||||
|
/// add extra stops to your gradient, which can increase the file size.
|
||||||
#[ty(scope)]
|
#[ty(scope)]
|
||||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum Gradient {
|
pub enum Gradient {
|
||||||
|
BIN
tests/ref/visualize/gradient-hue-rotation.png
Normal file
BIN
tests/ref/visualize/gradient-hue-rotation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
66
tests/typ/visualize/gradient-hue-rotation.typ
Normal file
66
tests/typ/visualize/gradient-hue-rotation.typ
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// Tests whether hue rotation works correctly.
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in Oklab space for reference.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 30pt,
|
||||||
|
fill: gradient.linear(red, purple, space: oklab)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in OkLCH space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 30pt,
|
||||||
|
fill: gradient.linear(red, purple, space: oklch)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in HSV space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 30pt,
|
||||||
|
fill: gradient.linear(red, purple, space: color.hsv)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in HSL space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 30pt,
|
||||||
|
fill: gradient.linear(red, purple, space: color.hsl)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in Oklab space for reference.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 100pt,
|
||||||
|
fill: gradient.conic(red, purple, space: oklab)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in OkLCH space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 100pt,
|
||||||
|
fill: gradient.conic(red, purple, space: oklch)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in HSV space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 100pt,
|
||||||
|
fill: gradient.conic(red, purple, space: color.hsv)
|
||||||
|
)
|
||||||
|
|
||||||
|
---
|
||||||
|
// Test in HSL space.
|
||||||
|
#set page(
|
||||||
|
width: 100pt,
|
||||||
|
height: 100pt,
|
||||||
|
fill: gradient.conic(red, purple, space: color.hsl)
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user