From d869a07d2dbdfb5f1952d2d574f6848d21e7f68e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20d=27Herbais=20de=20Thun?= Date: Wed, 13 Dec 2023 14:35:56 +0100 Subject: [PATCH] Remove HSV and HSL color spaces from PDF export (#2927) Co-authored-by: EpicEricEE --- crates/typst-pdf/src/color.rs | 116 +--------- crates/typst-pdf/src/gradient.rs | 209 ++++++------------ crates/typst-pdf/src/postscript/hsl.ps | 63 ------ crates/typst-pdf/src/postscript/hsv.ps | 62 ------ crates/typst/src/visualize/gradient.rs | 14 ++ tests/ref/visualize/gradient-hue-rotation.png | Bin 0 -> 32558 bytes tests/typ/visualize/gradient-hue-rotation.typ | 66 ++++++ 7 files changed, 147 insertions(+), 383 deletions(-) delete mode 100644 crates/typst-pdf/src/postscript/hsl.ps delete mode 100644 crates/typst-pdf/src/postscript/hsv.ps create mode 100644 tests/ref/visualize/gradient-hue-rotation.png create mode 100644 tests/typ/visualize/gradient-hue-rotation.typ diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs index a758d935d..3d90926f9 100644 --- a/crates/typst-pdf/src/color.rs +++ b/crates/typst-pdf/src/color.rs @@ -10,20 +10,12 @@ use crate::page::{PageContext, Transforms}; pub const SRGB: Name<'static> = Name(b"srgb"); pub const D65_GRAY: Name<'static> = Name(b"d65gray"); 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"); // 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"); -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. static SRGB_ICC_DEFLATED: Lazy> = @@ -34,10 +26,6 @@ static GRAY_ICC_DEFLATED: Lazy> = // The PostScript functions for color spaces. static OKLAB_DEFLATED: Lazy> = Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes())); -static HSV_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes())); -static HSL_DEFLATED: Lazy> = - Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes())); /// The color spaces present in the PDF document #[derive(Default)] @@ -45,8 +33,6 @@ pub struct ColorSpaces { oklab: Option, srgb: Option, d65_gray: Option, - hsv: Option, - hsl: Option, use_linear_rgb: bool, } @@ -70,24 +56,6 @@ impl ColorSpaces { *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. pub fn linear_rgb(&mut self) { self.use_linear_rgb = true; @@ -101,7 +69,7 @@ impl ColorSpaces { alloc: &mut Ref, ) { match color_space { - ColorSpace::Oklab => { + ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => { let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), 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(), } } @@ -151,14 +107,6 @@ impl ColorSpaces { 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 { self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc); } @@ -176,24 +124,6 @@ impl ColorSpaces { .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. if let Some(srgb) = self.srgb { chunk @@ -255,7 +185,7 @@ pub trait ColorEncode { impl ColorEncode for ColorSpace { fn encode(&self, color: Color) -> [f32; 4] { match self { - ColorSpace::Oklab | ColorSpace::Oklch => { + ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => { 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. let c = c.clamp(0.0, 0.5); @@ -264,15 +194,7 @@ impl ColorEncode for ColorSpace { let b = c * h.to_radians().sin(); [l, a + 0.5, b + 0.5, alpha] } - ColorSpace::Hsl => { - 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(), + _ => color.to_space(*self).to_vec4(), } } } @@ -315,7 +237,7 @@ impl PaintEncode for Color { ctx.content.set_fill_color([l]); } // 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.set_fill_color_space(OKLAB); @@ -342,20 +264,6 @@ impl PaintEncode for Color { let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); 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]); } // 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.set_stroke_color_space(OKLAB); @@ -396,20 +304,6 @@ impl PaintEncode for Color { let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); 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]); - } } } } diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs index b12ac53fa..0882a70ee 100644 --- a/crates/typst-pdf/src/gradient.rs +++ b/crates/typst-pdf/src/gradient.rs @@ -8,7 +8,7 @@ use pdf_writer::{Filter, Finish, Name, Ref}; use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; use typst::util::Numeric; use typst::visualize::{ - Color, ColorSpace, ConicGradient, Gradient, RelativeTo, WeightedColor, + Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, }; use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; @@ -49,7 +49,13 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) { ctx.colors .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() { Quadrant::First => (0.0, 0.0, cos, 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), }; - 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 .anti_alias(gradient.anti_alias()) .function(shading_function) @@ -100,7 +100,7 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) { shading_pattern } 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 mut stream_shading = @@ -148,73 +148,20 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref { for window in gradient.stops_ref().windows(2) { let (first, second) = (window[0], window[1]); - // Skip stops with the same position. - if first.1.get() == second.1.get() { - continue; - } + // If we have a hue index, we will create several stops in-between + // to make the gradient smoother without interpolation issues with + // 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, - // we need to create two separate stops. - if gradient.space() == ColorSpace::Hsl || gradient.space() == ColorSpace::Hsv { - let t1 = first.1.get() as f32; - let t2 = second.1.get() as f32; - 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; - } + let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t))); + functions.push(single_gradient(ctx, last_c, c, ColorSpace::Oklab)); + bounds.push(real_t as f32); + encode.extend([0.0, 1.0]); + last_c = c; } } @@ -427,108 +374,76 @@ fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, } #[comemo::memoize] -fn compute_vertex_stream(conic: &ConicGradient, aspect_ratio: Ratio) -> Arc> { +fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc> { + let Gradient::Conic(conic) = gradient else { unreachable!() }; + // Generated vertices for the Coons patches let mut vertices = Vec::new(); // Correct the gradient's angle 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) { 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 { + write_patch( + &mut vertices, + t0.get() as f32, + t1.get() as f32, + encode_space.convert(c0), + encode_space.convert(c1), + angle, + ); 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() { let t_next = (t_x + dt).min(t1.get()); - let t1 = slope * (t_x - t0.get()); - let t2 = slope * (t_next - t0.get()); - - // We don't use `Gradient::sample` to avoid issues with sharp gradients. + // The current progress in the current window. + let t = |t| (t - t0.get()) / (t1.get() - t0.get()); 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, ) .unwrap(); 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, ) .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( &mut vertices, t_x as f32, t_next as f32, - conic.space.convert(c), - conic.space.convert(c_next), + encode_space.convert(c), + encode_space.convert(c_next), angle, ); diff --git a/crates/typst-pdf/src/postscript/hsl.ps b/crates/typst-pdf/src/postscript/hsl.ps deleted file mode 100644 index 740bc3ed7..000000000 --- a/crates/typst-pdf/src/postscript/hsl.ps +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/crates/typst-pdf/src/postscript/hsv.ps b/crates/typst-pdf/src/postscript/hsv.ps deleted file mode 100644 index b29adf11d..000000000 --- a/crates/typst-pdf/src/postscript/hsv.ps +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/crates/typst/src/visualize/gradient.rs b/crates/typst/src/visualize/gradient.rs index 3848b4994..623cc368b 100644 --- a/crates/typst/src/visualize/gradient.rs +++ b/crates/typst/src/visualize/gradient.rs @@ -161,6 +161,20 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// # Presets /// Typst predefines color maps that you can use with your gradients. See the /// [`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)] #[derive(Clone, PartialEq, Eq, Hash)] pub enum Gradient { diff --git a/tests/ref/visualize/gradient-hue-rotation.png b/tests/ref/visualize/gradient-hue-rotation.png new file mode 100644 index 0000000000000000000000000000000000000000..2d786f710f597908fd6a98af45ab3e902871c0d9 GIT binary patch literal 32558 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øI7lNbT#fGp9sjC4JBn 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