use pdf_writer::types::{FunctionShadingType, MaskType};
use pdf_writer::{Chunk, Content, Filter, Finish, Name, Ref};
use usvg::{Paint, Rect, Transform};
use crate::util::context::Context;
use crate::util::helper::{
bbox_to_non_zero_rect, NameExt, RectExt, StopExt, TransformExt,
};
use crate::util::resources::ResourceContainer;
#[derive(Copy, Clone)]
pub struct Stop<const COUNT: usize> {
pub color: [f32; COUNT],
pub offset: f32,
}
struct GradientProperties {
coords: Vec<f32>,
shading_type: FunctionShadingType,
stops: Vec<usvg::Stop>,
transform: Transform,
}
impl GradientProperties {
fn try_from_paint(paint: &Paint) -> Option<Self> {
match paint {
Paint::LinearGradient(l) => Some(Self {
coords: vec![l.x1(), l.y1(), l.x2(), l.y2()],
shading_type: FunctionShadingType::Axial,
stops: Vec::from(l.stops()),
transform: l.transform(),
}),
Paint::RadialGradient(r) => Some(Self {
coords: vec![r.fx(), r.fy(), 0.0, r.cx(), r.cy(), r.r().get()],
shading_type: FunctionShadingType::Radial,
stops: Vec::from(r.stops()),
transform: r.transform(),
}),
_ => None,
}
}
}
pub fn create_shading_pattern(
paint: &Paint,
chunk: &mut Chunk,
ctx: &mut Context,
accumulated_transform: &Transform,
) -> Ref {
let properties = GradientProperties::try_from_paint(paint).unwrap();
shading_pattern(&properties, chunk, ctx, accumulated_transform)
}
pub fn create_shading_soft_mask(
paint: &Paint,
chunk: &mut Chunk,
ctx: &mut Context,
bbox: Rect,
) -> Option<Ref> {
let properties = GradientProperties::try_from_paint(paint).unwrap();
if properties.stops.iter().any(|stop| stop.opacity().get() < 1.0) {
Some(shading_soft_mask(&properties, chunk, ctx, bbox))
} else {
None
}
}
fn shading_pattern(
properties: &GradientProperties,
chunk: &mut Chunk,
ctx: &mut Context,
accumulated_transform: &Transform,
) -> Ref {
let pattern_ref = ctx.alloc_ref();
let matrix = accumulated_transform.pre_concat(properties.transform);
let shading_ref = shading_function(properties, chunk, ctx, false);
let mut shading_pattern = chunk.shading_pattern(pattern_ref);
shading_pattern.pair(Name(b"Shading"), shading_ref);
shading_pattern.matrix(matrix.to_pdf_transform());
shading_pattern.finish();
pattern_ref
}
fn shading_soft_mask(
properties: &GradientProperties,
chunk: &mut Chunk,
ctx: &mut Context,
bbox: Rect,
) -> Ref {
let mut rc = ResourceContainer::new();
let x_object_id = ctx.alloc_ref();
let shading_ref = shading_function(properties, chunk, ctx, true);
let shading_name = rc.add_shading(shading_ref);
let bbox = bbox_to_non_zero_rect(Some(bbox)).to_pdf_rect();
let transform = properties.transform;
let mut content = Content::new();
content.transform(transform.to_pdf_transform());
content.shading(shading_name.to_pdf_name());
let content_stream = ctx.finish_content(content);
let mut x_object = chunk.form_xobject(x_object_id, &content_stream);
rc.finish(&mut x_object.resources());
x_object
.group()
.transparency()
.isolated(false)
.knockout(false)
.color_space()
.icc_based(ctx.sgray_ref());
if ctx.options.compress {
x_object.filter(Filter::FlateDecode);
}
x_object.bbox(bbox);
x_object.finish();
let gs_ref = ctx.alloc_ref();
let mut gs = chunk.ext_graphics(gs_ref);
gs.soft_mask()
.subtype(MaskType::Luminosity)
.group(x_object_id)
.finish();
gs_ref
}
fn shading_function(
properties: &GradientProperties,
chunk: &mut Chunk,
ctx: &mut Context,
use_opacities: bool,
) -> Ref {
let shading_ref = ctx.alloc_ref();
let function_ref = function(&properties.stops, chunk, ctx, use_opacities);
let mut shading = chunk.function_shading(shading_ref);
shading.shading_type(properties.shading_type);
if use_opacities {
shading.color_space().icc_based(ctx.sgray_ref());
} else {
shading.color_space().icc_based(ctx.srgb_ref());
}
shading.function(function_ref);
shading.coords(properties.coords.iter().copied());
shading.extend([true, true]);
shading.finish();
shading_ref
}
fn function(
stops: &[usvg::Stop],
chunk: &mut Chunk,
ctx: &mut Context,
use_opacities: bool,
) -> Ref {
debug_assert!(stops.len() > 1);
fn pad_stops<const COUNT: usize>(mut stops: Vec<Stop<COUNT>>) -> Vec<Stop<COUNT>> {
if let Some(first) = stops.first() {
if first.offset != 0.0 {
let mut new_stop = *first;
new_stop.offset = 0.0;
stops.insert(0, new_stop);
}
}
if let Some(last) = stops.last() {
if last.offset != 1.0 {
let mut new_stop = *last;
new_stop.offset = 1.0;
stops.push(new_stop);
}
}
stops
}
if use_opacities {
let stops =
pad_stops(stops.iter().map(|s| s.opacity_stops()).collect::<Vec<Stop<1>>>());
select_function(&stops, chunk, ctx)
} else {
let stops =
pad_stops(stops.iter().map(|s| s.color_stops()).collect::<Vec<Stop<3>>>());
select_function(&stops, chunk, ctx)
}
}
fn select_function<const COUNT: usize>(
stops: &[Stop<COUNT>],
chunk: &mut Chunk,
ctx: &mut Context,
) -> Ref {
if stops.len() == 2 {
exponential_function(&stops[0], &stops[1], chunk, ctx)
} else {
stitching_function(stops, chunk, ctx)
}
}
fn stitching_function<const COUNT: usize>(
stops: &[Stop<COUNT>],
chunk: &mut Chunk,
ctx: &mut Context,
) -> Ref {
assert!(!stops.is_empty());
let reference = ctx.alloc_ref();
let mut functions = vec![];
let mut bounds = vec![];
let mut encode = vec![];
for window in stops.windows(2) {
let (first, second) = (&window[0], &window[1]);
bounds.push(second.offset);
functions.push(exponential_function(first, second, chunk, ctx));
encode.extend([0.0, 1.0]);
}
bounds.pop();
let mut stitching_function = chunk.stitching_function(reference);
stitching_function.domain([0.0, 1.0]);
stitching_function.range(get_function_range(COUNT));
stitching_function.functions(functions);
stitching_function.bounds(bounds);
stitching_function.encode(encode);
reference
}
fn exponential_function<const COUNT: usize>(
first_stop: &Stop<COUNT>,
second_stop: &Stop<COUNT>,
chunk: &mut Chunk,
ctx: &mut Context,
) -> Ref {
let reference = ctx.alloc_ref();
let mut exp = chunk.exponential_function(reference);
exp.range(get_function_range(COUNT));
exp.c0(first_stop.color);
exp.c1(second_stop.color);
exp.domain([0.0, 1.0]);
exp.n(1.0);
exp.finish();
reference
}
fn get_function_range(count: usize) -> Vec<f32> {
[0.0, 1.0].repeat(count)
}