use std::collections::BTreeSet;
use pdf_writer::Content;
use zenith_core::{AssetProvider, FontProvider};
use zenith_scene::{
Color, FitMode, ImageClip, Paint as ScenePaint, Scene, SceneCommand, StrokeAlign,
};
use super::color;
use super::font::FontPlan;
use super::geometry::{ellipse_path, poly_path, rounded_rect_path};
use super::gradient::{AxialGradient, resolve as resolve_gradient};
use super::image::{DecodedImage, decode_for_pdf};
#[derive(Default)]
pub(super) struct PageResources {
pub(super) alphas: Vec<u8>,
pub(super) gradients: Vec<AxialGradient>,
pub(super) images: Vec<DecodedImage>,
pub(super) font_indices: BTreeSet<usize>,
pub(super) links: Vec<LinkAnnot>,
}
pub(super) struct LinkAnnot {
pub(super) x0: f64,
pub(super) y0: f64,
pub(super) x1: f64,
pub(super) y1: f64,
pub(super) url: String,
}
impl PageResources {
pub(super) fn intern_alpha(&mut self, a: u8) -> usize {
match self.alphas.binary_search(&a) {
Ok(i) => i,
Err(i) => {
self.alphas.insert(i, a);
i
}
}
}
}
pub(super) const ALPHA_PREFIX: &str = "ga";
pub(super) const SHADING_PREFIX: &str = "sh";
pub(super) const IMAGE_PREFIX: &str = "im";
pub(super) const FONT_PREFIX: &str = "f";
pub(super) fn translate(
scene: &Scene,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
font_plan: &FontPlan,
) -> (Content, PageResources) {
let mut content = Content::new();
let mut res = PageResources::default();
content.transform([1.0, 0.0, 0.0, -1.0, 0.0, scene.height as f32]);
let page = (scene.width, scene.height);
let mut effect_buf: Option<(u32, Vec<SceneCommand>)> = None;
for cmd in &scene.commands {
let is_open = is_effect_open(cmd);
let is_close = is_effect_close(cmd);
if let Some((depth, buffered)) = effect_buf.as_mut() {
buffered.push(cmd.clone());
if is_open {
*depth += 1;
} else if is_close {
*depth = depth.saturating_sub(1);
if *depth == 0
&& let Some((_, region)) = effect_buf.take()
{
super::raster_embed::embed_rasterized_region(
&mut content,
&mut res,
®ion,
page,
fonts,
assets,
font_plan,
);
}
}
continue;
}
if is_open && !is_empty_filter(cmd) {
effect_buf = Some((1, vec![cmd.clone()]));
continue;
}
emit_command(&mut content, &mut res, cmd, page, fonts, assets, font_plan);
}
(content, res)
}
enum EffectBracket {
Open,
Close,
None,
}
fn effect_bracket(cmd: &SceneCommand) -> EffectBracket {
match cmd {
SceneCommand::BeginShadow { .. }
| SceneCommand::BeginBlur { .. }
| SceneCommand::BeginFilter { .. }
| SceneCommand::BeginMask { .. } => EffectBracket::Open,
SceneCommand::EndShadow
| SceneCommand::EndBlur
| SceneCommand::EndFilter
| SceneCommand::EndMask => EffectBracket::Close,
SceneCommand::FillRect { .. }
| SceneCommand::StrokeRect { .. }
| SceneCommand::FillRoundedRect { .. }
| SceneCommand::StrokeRoundedRect { .. }
| SceneCommand::FillEllipse { .. }
| SceneCommand::StrokeEllipse { .. }
| SceneCommand::StrokeLine { .. }
| SceneCommand::FillPolygon { .. }
| SceneCommand::StrokePolyline { .. }
| SceneCommand::DrawImage { .. }
| SceneCommand::DrawSvgAsset { .. }
| SceneCommand::DrawGlyphRun { .. }
| SceneCommand::PushClip { .. }
| SceneCommand::PopClip
| SceneCommand::PushLayer { .. }
| SceneCommand::PopLayer
| SceneCommand::PushTransform { .. }
| SceneCommand::PopTransform => EffectBracket::None,
}
}
fn is_effect_open(cmd: &SceneCommand) -> bool {
matches!(effect_bracket(cmd), EffectBracket::Open)
}
fn is_effect_close(cmd: &SceneCommand) -> bool {
matches!(effect_bracket(cmd), EffectBracket::Close)
}
fn is_empty_filter(cmd: &SceneCommand) -> bool {
matches!(cmd, SceneCommand::BeginFilter { filters } if filters.is_empty())
}
pub(super) fn apply_alpha(content: &mut Content, res: &mut PageResources, color: &Color) {
if color.a == 255 {
return;
}
let idx = res.intern_alpha(color.a);
content.set_parameters(name(ALPHA_PREFIX, idx).as_name());
}
fn fill_region<F: Fn(&mut Content) -> bool>(
content: &mut Content,
res: &mut PageResources,
paint: &ScenePaint,
bbox: (f64, f64, f64, f64),
even_odd: bool,
build_path: F,
) {
let fill = |content: &mut Content, produced: bool| {
if produced {
if even_odd {
content.fill_even_odd();
} else {
content.fill_nonzero();
}
} else {
content.end_path();
}
};
match paint {
ScenePaint::Solid { color } => {
content.save_state();
apply_alpha(content, res, color);
color::set_fill(content, color);
let produced = build_path(content);
fill(content, produced);
content.restore_state();
}
ScenePaint::Gradient(gradient) if gradient.radial => {
if let Some(first) = gradient.stops.first() {
content.save_state();
apply_alpha(content, res, &first.color);
color::set_fill(content, &first.color);
let produced = build_path(content);
fill(content, produced);
content.restore_state();
}
}
ScenePaint::Gradient(gradient) => {
let (x, y, w, h) = bbox;
if let Some(g) = resolve_gradient(x, y, w, h, gradient) {
let id = push_gradient(res, g);
content.save_state();
if build_path(content) {
if even_odd {
content.clip_even_odd();
} else {
content.clip_nonzero();
}
content.end_path();
content.shading(name(SHADING_PREFIX, id).as_name());
} else {
content.end_path();
}
content.restore_state();
}
}
}
}
fn poly_bbox(points: &[f64]) -> (f64, f64, f64, f64) {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for pair in points.chunks_exact(2) {
min_x = min_x.min(pair[0]);
max_x = max_x.max(pair[0]);
min_y = min_y.min(pair[1]);
max_y = max_y.max(pair[1]);
}
(min_x, min_y, max_x - min_x, max_y - min_y)
}
pub(super) fn emit_command(
content: &mut Content,
res: &mut PageResources,
cmd: &SceneCommand,
page: (f64, f64),
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
font_plan: &FontPlan,
) {
match cmd {
SceneCommand::FillRect { x, y, w, h, paint } => {
if !rect_ok(*x, *y, *w, *h) {
return;
}
fill_region(content, res, paint, (*x, *y, *w, *h), false, |c| {
c.rect(*x as f32, *y as f32, *w as f32, *h as f32);
true
});
}
SceneCommand::StrokeRect {
x,
y,
w,
h,
color,
stroke_width,
..
} => {
if !rect_ok(*x, *y, *w, *h) || !finite(*stroke_width) {
return;
}
content.save_state();
apply_alpha(content, res, color);
color::set_stroke(content, color);
content.set_line_width(*stroke_width as f32);
content.rect(*x as f32, *y as f32, *w as f32, *h as f32);
content.stroke();
content.restore_state();
}
SceneCommand::FillRoundedRect {
x,
y,
w,
h,
radius,
radii,
paint,
} => {
if !rect_ok(*x, *y, *w, *h) || !finite(*radius) {
return;
}
let corner_radii = radii.unwrap_or([*radius; 4]);
fill_region(content, res, paint, (*x, *y, *w, *h), false, |c| {
rounded_rect_path(c, *x, *y, *w, *h, corner_radii);
true
});
}
SceneCommand::StrokeRoundedRect {
x,
y,
w,
h,
radius,
radii,
color,
stroke_width,
..
} => {
if !rect_ok(*x, *y, *w, *h) || !finite(*radius) || !finite(*stroke_width) {
return;
}
let corner_radii = radii.unwrap_or([*radius; 4]);
content.save_state();
apply_alpha(content, res, color);
color::set_stroke(content, color);
content.set_line_width(*stroke_width as f32);
rounded_rect_path(content, *x, *y, *w, *h, corner_radii);
content.stroke();
content.restore_state();
}
SceneCommand::FillEllipse {
x,
y,
w,
h,
rx,
ry,
paint,
} => {
if !rect_ok(*x, *y, *w, *h) {
return;
}
fill_region(content, res, paint, (*x, *y, *w, *h), false, |c| {
ellipse_path(c, *x, *y, *w, *h, *rx, *ry);
true
});
}
SceneCommand::StrokeEllipse {
x,
y,
w,
h,
rx,
ry,
color,
stroke_width,
..
} => {
if !rect_ok(*x, *y, *w, *h) || !finite(*stroke_width) {
return;
}
content.save_state();
apply_alpha(content, res, color);
color::set_stroke(content, color);
content.set_line_width(*stroke_width as f32);
ellipse_path(content, *x, *y, *w, *h, *rx, *ry);
content.stroke();
content.restore_state();
}
SceneCommand::StrokeLine {
x1,
y1,
x2,
y2,
color,
stroke_width,
..
} => {
if !finite(*x1)
|| !finite(*y1)
|| !finite(*x2)
|| !finite(*y2)
|| !finite(*stroke_width)
{
return;
}
content.save_state();
apply_alpha(content, res, color);
color::set_stroke(content, color);
content.set_line_width(*stroke_width as f32);
content.move_to(*x1 as f32, *y1 as f32);
content.line_to(*x2 as f32, *y2 as f32);
content.stroke();
content.restore_state();
}
SceneCommand::FillPolygon {
points,
paint,
even_odd,
} => {
if points.len() < 6 || points.iter().any(|v| !v.is_finite()) {
return;
}
let bbox = poly_bbox(points);
fill_region(content, res, paint, bbox, *even_odd, |c| {
poly_path(c, points, true)
});
}
SceneCommand::StrokePolyline {
points,
color,
stroke_width,
closed,
align,
fill_even_odd,
} => {
if points.len() < 4 || points.iter().any(|v| !v.is_finite()) || !finite(*stroke_width) {
return;
}
let aligned = *closed && !matches!(align, StrokeAlign::Center);
content.save_state();
apply_alpha(content, res, color);
color::set_stroke(content, color);
if aligned {
match align {
StrokeAlign::Inside => {
if !poly_path(content, points, true) {
content.end_path();
content.restore_state();
return;
}
if *fill_even_odd {
content.clip_even_odd();
} else {
content.clip_nonzero();
}
content.end_path();
}
StrokeAlign::Outside => {
let (pw, ph) = page;
let m = pw.max(ph).max(1.0); content.move_to(-m as f32, -m as f32);
content.line_to((pw + m) as f32, -m as f32);
content.line_to((pw + m) as f32, (ph + m) as f32);
content.line_to(-m as f32, (ph + m) as f32);
content.close_path();
if !poly_path(content, points, true) {
content.end_path();
content.restore_state();
return;
}
content.clip_even_odd();
content.end_path();
}
StrokeAlign::Center => {}
}
content.set_line_width((*stroke_width * 2.0) as f32);
if poly_path(content, points, true) {
content.stroke();
} else {
content.end_path();
}
} else {
content.set_line_width(*stroke_width as f32);
if poly_path(content, points, *closed) {
content.stroke();
} else {
content.end_path();
}
}
content.restore_state();
}
SceneCommand::DrawGlyphRun {
x,
y,
font_id,
font_size,
color,
stroke_color: _,
stroke_width: _,
link,
selectable,
glyphs,
} => {
super::glyph::emit_glyph_run(
content,
res,
fonts,
font_plan,
super::glyph::GlyphRun {
x: *x,
y: *y,
font_id,
font_size: *font_size,
color,
link: link.as_deref(),
selectable: *selectable,
glyphs,
},
);
}
SceneCommand::DrawImage {
x,
y,
w,
h,
asset_id,
fit,
pos_x,
pos_y,
opacity,
clip_shape,
src_rect: _,
} => {
emit_image(
content,
res,
fonts,
assets,
ImageDraw {
x: *x,
y: *y,
w: *w,
h: *h,
asset_id,
fit: *fit,
pos_x: *pos_x,
pos_y: *pos_y,
opacity: *opacity,
clip_shape,
},
);
}
SceneCommand::DrawSvgAsset { .. } => {}
SceneCommand::PushClip { x, y, w, h } => {
content.save_state();
content.rect(*x as f32, *y as f32, *w as f32, *h as f32);
content.clip_nonzero();
content.end_path();
}
SceneCommand::PopClip => {
content.restore_state();
}
SceneCommand::PushTransform { angle_deg, cx, cy } => {
content.save_state();
let theta = (*angle_deg).to_radians();
let (s, c) = (theta.sin() as f32, theta.cos() as f32);
let (cx, cy) = (*cx as f32, *cy as f32);
content.transform([c, s, -s, c, cx - c * cx + s * cy, cy - s * cx - c * cy]);
}
SceneCommand::PopTransform => {
content.restore_state();
}
SceneCommand::PushLayer { .. } => {
content.save_state();
}
SceneCommand::PopLayer => {
content.restore_state();
}
SceneCommand::BeginShadow { .. } => {}
SceneCommand::EndShadow => {}
SceneCommand::BeginBlur { .. } => {}
SceneCommand::EndBlur => {}
SceneCommand::BeginFilter { .. } => {}
SceneCommand::EndFilter => {}
SceneCommand::BeginMask { .. } => {}
SceneCommand::EndMask => {}
}
}
pub(super) fn push_gradient(res: &mut PageResources, g: AxialGradient) -> usize {
let id = res.gradients.len();
res.gradients.push(g);
id
}
#[derive(Clone, Copy)]
struct ImageDraw<'a> {
x: f64,
y: f64,
w: f64,
h: f64,
asset_id: &'a str,
fit: FitMode,
pos_x: f64,
pos_y: f64,
opacity: f64,
clip_shape: &'a Option<ImageClip>,
}
fn emit_image(
content: &mut Content,
res: &mut PageResources,
fonts: &dyn FontProvider,
assets: &dyn AssetProvider,
draw: ImageDraw<'_>,
) {
let ImageDraw {
x,
y,
w,
h,
asset_id,
fit,
pos_x,
pos_y,
opacity,
clip_shape,
} = draw;
if !rect_ok(x, y, w, h) {
return;
}
let Some(asset) = assets.by_id(asset_id) else {
return;
};
match asset.kind {
zenith_core::AssetKind::Image => {}
zenith_core::AssetKind::Svg => {
super::svg::emit_svg(
content,
res,
fonts,
&asset.bytes,
super::svg::SvgPlacement {
x,
y,
w,
h,
fit,
pos_x,
pos_y,
opacity,
clip_shape,
},
);
return;
}
zenith_core::AssetKind::Font | zenith_core::AssetKind::Unknown(_) => return,
}
let Some(decoded) = decode_for_pdf(&asset.bytes) else {
return;
};
let (sw, sh) = (f64::from(decoded.width), f64::from(decoded.height));
if !(sw > 0.0 && sh > 0.0) {
return;
}
let (sx, sy, tx, ty) = match fit {
FitMode::Stretch => (w / sw, h / sh, x, y),
FitMode::Contain => {
let s = (w / sw).min(h / sh);
let (rw, rh) = (sw * s, sh * s);
(
s,
s,
x + (w - rw) * pos_x / 100.0,
y + (h - rh) * pos_y / 100.0,
)
}
FitMode::Cover => {
let s = (w / sw).max(h / sh);
let (rw, rh) = (sw * s, sh * s);
(
s,
s,
x - (rw - w) * pos_x / 100.0,
y - (rh - h) * pos_y / 100.0,
)
}
FitMode::None => (
1.0,
1.0,
x - (sw - w) * pos_x / 100.0,
y - (sh - h) * pos_y / 100.0,
),
};
if !finite(sx) || !finite(sy) || !finite(tx) || !finite(ty) || sx <= 0.0 || sy <= 0.0 {
return;
}
let id = res.images.len();
res.images.push(decoded);
content.save_state();
let op = (opacity as f32).clamp(0.0, 1.0);
if op < 1.0 {
let a = (op * 255.0).round().clamp(0.0, 255.0) as u8;
let aidx = res.intern_alpha(a);
content.set_parameters(name(ALPHA_PREFIX, aidx).as_name());
}
match clip_shape {
None => {
content.rect(x as f32, y as f32, w as f32, h as f32);
content.clip_nonzero();
content.end_path();
}
Some(ImageClip::Ellipse) => {
ellipse_path(content, x, y, w, h, None, None);
content.clip_nonzero();
content.end_path();
}
Some(ImageClip::RoundedRect { radius }) => {
rounded_rect_path(content, x, y, w, h, [*radius; 4]);
content.clip_nonzero();
content.end_path();
}
}
let iw = (sw * sx) as f32;
let ih = (sh * sy) as f32;
content.transform([iw, 0.0, 0.0, -ih, tx as f32, ty as f32 + ih]);
content.x_object(name(IMAGE_PREFIX, id).as_name());
content.restore_state();
}
#[inline]
fn finite(v: f64) -> bool {
v.is_finite()
}
#[inline]
fn rect_ok(x: f64, y: f64, w: f64, h: f64) -> bool {
finite(x) && finite(y) && finite(w) && finite(h) && w > 0.0 && h > 0.0
}
pub(super) struct ResName {
buf: [u8; 24],
len: usize,
}
impl ResName {
pub(super) fn as_name(&self) -> pdf_writer::Name<'_> {
pdf_writer::Name(&self.buf[..self.len])
}
}
pub(super) fn name(prefix: &str, index: usize) -> ResName {
use std::io::Write;
let mut buf = [0u8; 24];
let mut cursor = std::io::Cursor::new(&mut buf[..]);
let _ = write!(cursor, "{prefix}{index}");
let len = cursor.position() as usize;
ResName { buf, len }
}