use std::sync::LazyLock;
use arrayvec::ArrayVec;
use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
use typst_library::diag::{bail, SourceResult};
use typst_library::visualize::{Color, ColorSpace, Paint};
use typst_syntax::Span;
use crate::{content, deflate, PdfChunk, PdfOptions, Renumber, WithResources};
pub const SRGB: Name<'static> = Name(b"srgb");
pub const D65_GRAY: Name<'static> = Name(b"d65gray");
pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb");
static SRGB_ICC_DEFLATED: LazyLock<Vec<u8>> =
LazyLock::new(|| deflate(typst_assets::icc::S_RGB_V4));
static GRAY_ICC_DEFLATED: LazyLock<Vec<u8>> =
LazyLock::new(|| deflate(typst_assets::icc::S_GREY_V4));
#[derive(Default)]
pub struct ColorSpaces {
use_srgb: bool,
use_d65_gray: bool,
use_linear_rgb: bool,
}
impl ColorSpaces {
pub fn mark_as_used(&mut self, color_space: ColorSpace) {
match color_space {
ColorSpace::Oklch
| ColorSpace::Oklab
| ColorSpace::Hsl
| ColorSpace::Hsv
| ColorSpace::Srgb => {
self.use_srgb = true;
}
ColorSpace::D65Gray => {
self.use_d65_gray = true;
}
ColorSpace::LinearRgb => {
self.use_linear_rgb = true;
}
ColorSpace::Cmyk => {}
}
}
pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) {
if self.use_srgb {
write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs);
}
if self.use_d65_gray {
write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), refs);
}
if self.use_linear_rgb {
write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), refs);
}
}
pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) {
if let Some(id) = refs.srgb {
chunk
.icc_profile(id, &SRGB_ICC_DEFLATED)
.n(3)
.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.filter(Filter::FlateDecode);
}
if let Some(id) = refs.d65_gray {
chunk
.icc_profile(id, &GRAY_ICC_DEFLATED)
.n(1)
.range([0.0, 1.0])
.filter(Filter::FlateDecode);
}
}
pub fn merge(&mut self, other: &Self) {
self.use_d65_gray |= other.use_d65_gray;
self.use_linear_rgb |= other.use_linear_rgb;
self.use_srgb |= other.use_srgb;
}
}
pub fn write(
color_space: ColorSpace,
writer: writers::ColorSpace,
refs: &ColorFunctionRefs,
) {
match color_space {
ColorSpace::Srgb
| ColorSpace::Oklab
| ColorSpace::Hsl
| ColorSpace::Hsv
| ColorSpace::Oklch => writer.icc_based(refs.srgb.unwrap()),
ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()),
ColorSpace::LinearRgb => {
writer.cal_rgb(
[0.9505, 1.0, 1.0888],
None,
Some([1.0, 1.0, 1.0]),
Some([
0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, 0.9505,
]),
);
}
ColorSpace::Cmyk => writer.device_cmyk(),
}
}
pub struct ColorFunctionRefs {
pub srgb: Option<Ref>,
d65_gray: Option<Ref>,
}
impl Renumber for ColorFunctionRefs {
fn renumber(&mut self, offset: i32) {
if let Some(r) = &mut self.srgb {
r.renumber(offset);
}
if let Some(r) = &mut self.d65_gray {
r.renumber(offset);
}
}
}
pub fn alloc_color_functions_refs(
context: &WithResources,
) -> SourceResult<(PdfChunk, ColorFunctionRefs)> {
let mut chunk = PdfChunk::new();
let mut used_color_spaces = ColorSpaces::default();
if context.options.standards.pdfa {
used_color_spaces.mark_as_used(ColorSpace::Srgb);
}
context.resources.traverse(&mut |r| {
used_color_spaces.merge(&r.colors);
Ok(())
})?;
let refs = ColorFunctionRefs {
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 },
};
Ok((chunk, refs))
}
pub trait ColorEncode {
fn encode(&self, color: Color) -> [f32; 4];
}
impl ColorEncode for ColorSpace {
fn encode(&self, color: Color) -> [f32; 4] {
match self {
ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => {
color.to_space(ColorSpace::Srgb).to_vec4()
}
_ => color.to_space(*self).to_vec4(),
}
}
}
pub(super) trait PaintEncode {
fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) -> SourceResult<()>;
fn set_as_stroke(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) -> SourceResult<()>;
}
impl PaintEncode for Paint {
fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) -> SourceResult<()> {
match self {
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
Self::Tiling(tiling) => tiling.set_as_fill(ctx, on_text, transforms),
}
}
fn set_as_stroke(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) -> SourceResult<()> {
match self {
Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms),
Self::Tiling(tiling) => tiling.set_as_stroke(ctx, on_text, transforms),
}
}
}
impl PaintEncode for Color {
fn set_as_fill(
&self,
ctx: &mut content::Builder,
_: bool,
_: content::Transforms,
) -> SourceResult<()> {
match self {
Color::Luma(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::D65Gray);
ctx.set_fill_color_space(D65_GRAY);
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
ctx.content.set_fill_color([l]);
}
Color::LinearRgb(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_fill_color_space(LINEAR_SRGB);
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_fill_color([r, g, b]);
}
Color::Rgb(_)
| Color::Oklab(_)
| Color::Oklch(_)
| Color::Hsl(_)
| Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_fill_color_space(SRGB);
let [r, g, b, _] = ColorSpace::Srgb.encode(*self);
ctx.content.set_fill_color([r, g, b]);
}
Color::Cmyk(_) => {
check_cmyk_allowed(ctx.options)?;
ctx.reset_fill_color_space();
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
ctx.content.set_fill_cmyk(c, m, y, k);
}
}
Ok(())
}
fn set_as_stroke(
&self,
ctx: &mut content::Builder,
_: bool,
_: content::Transforms,
) -> SourceResult<()> {
match self {
Color::Luma(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::D65Gray);
ctx.set_stroke_color_space(D65_GRAY);
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
ctx.content.set_stroke_color([l]);
}
Color::LinearRgb(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_stroke_color_space(LINEAR_SRGB);
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_stroke_color([r, g, b]);
}
Color::Rgb(_)
| Color::Oklab(_)
| Color::Oklch(_)
| Color::Hsl(_)
| Color::Hsv(_) => {
ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_stroke_color_space(SRGB);
let [r, g, b, _] = ColorSpace::Srgb.encode(*self);
ctx.content.set_stroke_color([r, g, b]);
}
Color::Cmyk(_) => {
check_cmyk_allowed(ctx.options)?;
ctx.reset_stroke_color_space();
let [c, m, y, k] = ColorSpace::Cmyk.encode(*self);
ctx.content.set_stroke_cmyk(c, m, y, k);
}
}
Ok(())
}
}
pub(super) trait ColorSpaceExt {
fn range(self) -> &'static [f32];
fn convert<U: QuantizedColor>(self, color: Color) -> ArrayVec<U, 4>;
}
impl ColorSpaceExt for ColorSpace {
fn range(self) -> &'static [f32] {
match self {
ColorSpace::D65Gray => &[0.0, 1.0],
ColorSpace::Oklab => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::Oklch => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::LinearRgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::Srgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::Cmyk => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::Hsl => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
ColorSpace::Hsv => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0],
}
}
fn convert<U: QuantizedColor>(self, color: Color) -> ArrayVec<U, 4> {
let components = self.encode(color);
self.range()
.chunks(2)
.zip(components)
.map(|(range, component)| U::quantize(component, [range[0], range[1]]))
.collect()
}
}
pub(super) trait QuantizedColor {
fn quantize(color: f32, range: [f32; 2]) -> Self;
}
impl QuantizedColor for u16 {
fn quantize(color: f32, [min, max]: [f32; 2]) -> Self {
let value = (color - min) / (max - min);
(value * Self::MAX as f32).round().clamp(0.0, Self::MAX as f32) as Self
}
}
impl QuantizedColor for f32 {
fn quantize(color: f32, [min, max]: [f32; 2]) -> Self {
color.clamp(min, max)
}
}
pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> {
if options.standards.pdfa {
bail!(
Span::detached(),
"cmyk colors are not currently supported by PDF/A export"
);
}
Ok(())
}