use crate::resources::ResourceCollector;
use oxideav_core::vector::{
DashPattern, FillRule, LineCap, LineJoin, Paint, Path, PathCommand, Point, Rgba, Stroke,
Transform2D,
};
pub struct OpBuf {
bytes: Vec<u8>,
}
impl OpBuf {
pub fn new() -> Self {
Self { bytes: Vec::new() }
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
fn write(&mut self, s: &[u8]) {
self.bytes.extend_from_slice(s);
}
fn space(&mut self) {
self.bytes.push(b' ');
}
fn newline(&mut self) {
self.bytes.push(b'\n');
}
fn write_real(&mut self, f: f32) {
self.write(format_real(f as f64).as_bytes());
}
pub fn append_raw(&mut self, bytes: &[u8]) {
self.write(bytes);
}
}
impl Default for OpBuf {
fn default() -> Self {
Self::new()
}
}
pub fn save(op: &mut OpBuf) {
op.write(b"q");
op.newline();
}
pub fn restore(op: &mut OpBuf) {
op.write(b"Q");
op.newline();
}
pub fn concat_matrix(op: &mut OpBuf, t: &Transform2D) {
if t.is_identity() {
return;
}
op.write_real(t.a);
op.space();
op.write_real(t.b);
op.space();
op.write_real(t.c);
op.space();
op.write_real(t.d);
op.space();
op.write_real(t.e);
op.space();
op.write_real(t.f);
op.space();
op.write(b"cm");
op.newline();
}
pub fn set_ext_gstate(op: &mut OpBuf, name: &str) {
op.write(b"/");
op.write(name.as_bytes());
op.space();
op.write(b"gs");
op.newline();
}
pub fn emit_path(op: &mut OpBuf, path: &Path) {
let mut current = Point::default();
let mut subpath_start = Point::default();
for cmd in &path.commands {
match *cmd {
PathCommand::MoveTo(p) => {
op.write_real(p.x);
op.space();
op.write_real(p.y);
op.space();
op.write(b"m");
op.newline();
current = p;
subpath_start = p;
}
PathCommand::LineTo(p) => {
op.write_real(p.x);
op.space();
op.write_real(p.y);
op.space();
op.write(b"l");
op.newline();
current = p;
}
PathCommand::CubicCurveTo { c1, c2, end } => {
emit_cubic(op, c1, c2, end);
current = end;
}
PathCommand::QuadCurveTo { control, end } => {
let c1 = Point::new(
current.x + (2.0 / 3.0) * (control.x - current.x),
current.y + (2.0 / 3.0) * (control.y - current.y),
);
let c2 = Point::new(
end.x + (2.0 / 3.0) * (control.x - end.x),
end.y + (2.0 / 3.0) * (control.y - end.y),
);
emit_cubic(op, c1, c2, end);
current = end;
}
PathCommand::ArcTo {
rx,
ry,
x_axis_rot,
large_arc,
sweep,
end,
} => {
for (c1, c2, e) in crate::arc::svg_arc_to_cubics(
current, end, rx, ry, x_axis_rot, large_arc, sweep,
) {
emit_cubic(op, c1, c2, e);
}
current = end;
}
PathCommand::Close => {
op.write(b"h");
op.newline();
current = subpath_start;
}
_ => {}
}
}
}
fn emit_cubic(op: &mut OpBuf, c1: Point, c2: Point, end: Point) {
op.write_real(c1.x);
op.space();
op.write_real(c1.y);
op.space();
op.write_real(c2.x);
op.space();
op.write_real(c2.y);
op.space();
op.write_real(end.x);
op.space();
op.write_real(end.y);
op.space();
op.write(b"c");
op.newline();
}
pub enum PaintMode {
Fill,
Stroke,
FillStroke,
None,
}
pub fn paint(op: &mut OpBuf, mode: PaintMode, fill_rule: FillRule) {
let bytes: &[u8] = match (mode, fill_rule) {
(PaintMode::Fill, FillRule::NonZero) => b"f",
(PaintMode::Fill, FillRule::EvenOdd) => b"f*",
(PaintMode::Stroke, _) => b"S",
(PaintMode::FillStroke, FillRule::NonZero) => b"B",
(PaintMode::FillStroke, FillRule::EvenOdd) => b"B*",
(PaintMode::None, _) => b"n",
};
op.write(bytes);
op.newline();
}
pub fn emit_clip_marker(op: &mut OpBuf, fill_rule: FillRule) {
match fill_rule {
FillRule::NonZero => op.write(b"W"),
FillRule::EvenOdd => op.write(b"W*"),
}
op.newline();
paint(op, PaintMode::None, fill_rule);
}
pub fn set_fill_paint(op: &mut OpBuf, paint: &Paint, resources: &mut ResourceCollector) {
match paint {
Paint::Solid(rgba) => {
emit_rgb(op, *rgba, false);
}
Paint::LinearGradient(g) => {
let name = resources.add_linear_gradient(g);
emit_pattern(op, &name, false);
}
Paint::RadialGradient(g) => {
let name = resources.add_radial_gradient(g);
emit_pattern(op, &name, false);
}
_ => emit_rgb(op, Rgba::opaque(0, 0, 0), false),
}
}
pub fn set_stroke_paint(op: &mut OpBuf, paint: &Paint, resources: &mut ResourceCollector) {
match paint {
Paint::Solid(rgba) => {
emit_rgb(op, *rgba, true);
}
Paint::LinearGradient(g) => {
let name = resources.add_linear_gradient(g);
emit_pattern(op, &name, true);
}
Paint::RadialGradient(g) => {
let name = resources.add_radial_gradient(g);
emit_pattern(op, &name, true);
}
_ => emit_rgb(op, Rgba::opaque(0, 0, 0), true),
}
}
fn emit_rgb(op: &mut OpBuf, rgba: Rgba, stroke: bool) {
let r = rgba.r as f32 / 255.0;
let g = rgba.g as f32 / 255.0;
let b = rgba.b as f32 / 255.0;
op.write_real(r);
op.space();
op.write_real(g);
op.space();
op.write_real(b);
op.space();
op.write(if stroke { b"RG" } else { b"rg" });
op.newline();
}
fn emit_pattern(op: &mut OpBuf, name: &str, stroke: bool) {
op.write(if stroke {
b"/Pattern CS"
} else {
b"/Pattern cs"
});
op.newline();
op.write(b"/");
op.write(name.as_bytes());
op.space();
op.write(if stroke { b"SCN" } else { b"scn" });
op.newline();
}
pub fn set_stroke_style(op: &mut OpBuf, stroke: &Stroke, resources: &mut ResourceCollector) {
op.write_real(stroke.width);
op.space();
op.write(b"w");
op.newline();
let cap = match stroke.cap {
LineCap::Butt => 0,
LineCap::Round => 1,
LineCap::Square => 2,
};
op.write(format!("{} J", cap).as_bytes());
op.newline();
let join = match stroke.join {
LineJoin::Miter => 0,
LineJoin::Round => 1,
LineJoin::Bevel => 2,
};
op.write(format!("{} j", join).as_bytes());
op.newline();
op.write_real(stroke.miter_limit);
op.space();
op.write(b"M");
op.newline();
if let Some(dash) = &stroke.dash {
emit_dash(op, dash);
} else {
op.write(b"[] 0 d");
op.newline();
}
set_stroke_paint(op, &stroke.paint, resources);
}
fn emit_dash(op: &mut OpBuf, dash: &DashPattern) {
op.write(b"[");
for (i, seg) in dash.array.iter().enumerate() {
if i > 0 {
op.space();
}
op.write_real(*seg);
}
op.write(b"] ");
op.write_real(dash.offset);
op.space();
op.write(b"d");
op.newline();
}
pub fn format_real(f: f64) -> String {
if !f.is_finite() {
return "0".to_string();
}
if f.fract() == 0.0 && f.abs() < 1e16 {
return format!("{}", f as i64);
}
let s = format!("{:.6}", f);
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
if trimmed.is_empty() || trimmed == "-" {
"0".to_string()
} else {
trimmed.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::vector::PathNode;
#[test]
fn save_restore_produce_q_capital_q() {
let mut op = OpBuf::new();
save(&mut op);
restore(&mut op);
assert_eq!(op.into_bytes(), b"q\nQ\n");
}
#[test]
fn identity_matrix_emits_nothing() {
let mut op = OpBuf::new();
concat_matrix(&mut op, &Transform2D::identity());
assert!(op.as_bytes().is_empty());
}
#[test]
fn translate_matrix_emits_cm() {
let mut op = OpBuf::new();
concat_matrix(&mut op, &Transform2D::translate(5.0, 10.0));
assert_eq!(op.into_bytes(), b"1 0 0 1 5 10 cm\n");
}
#[test]
fn rect_path_emits_m_l_l_l_h() {
let mut p = Path::new();
p.move_to(Point::new(10.0, 10.0))
.line_to(Point::new(110.0, 10.0))
.line_to(Point::new(110.0, 60.0))
.line_to(Point::new(10.0, 60.0))
.close();
let mut op = OpBuf::new();
emit_path(&mut op, &p);
let s = String::from_utf8(op.into_bytes()).unwrap();
assert!(s.contains("10 10 m"));
assert!(s.contains("110 10 l"));
assert!(s.contains("110 60 l"));
assert!(s.contains("10 60 l"));
assert!(s.contains("h"));
}
#[test]
fn quad_lifts_to_cubic() {
let mut p = Path::new();
p.move_to(Point::new(0.0, 0.0))
.quad_to(Point::new(3.0, 9.0), Point::new(6.0, 0.0));
let mut op = OpBuf::new();
emit_path(&mut op, &p);
let s = String::from_utf8(op.into_bytes()).unwrap();
assert!(s.contains("2 6 4 6 6 0 c"));
}
#[test]
fn fill_paint_solid_emits_rgb() {
let mut op = OpBuf::new();
let mut res = ResourceCollector::new();
set_fill_paint(&mut op, &Paint::Solid(Rgba::opaque(255, 128, 0)), &mut res);
let s = String::from_utf8(op.into_bytes()).unwrap();
assert!(s.starts_with("1 0.501961 0 rg"));
}
#[test]
fn paint_modes() {
for (mode, rule, expected) in [
(PaintMode::Fill, FillRule::NonZero, "f\n"),
(PaintMode::Fill, FillRule::EvenOdd, "f*\n"),
(PaintMode::Stroke, FillRule::NonZero, "S\n"),
(PaintMode::FillStroke, FillRule::NonZero, "B\n"),
(PaintMode::FillStroke, FillRule::EvenOdd, "B*\n"),
(PaintMode::None, FillRule::NonZero, "n\n"),
] {
let mut op = OpBuf::new();
paint(&mut op, mode, rule);
assert_eq!(op.into_bytes(), expected.as_bytes());
}
}
#[test]
fn stroke_style_emits_w_j_join_miter_dash() {
let mut op = OpBuf::new();
let mut res = ResourceCollector::new();
let stroke = Stroke {
width: 2.5,
paint: Paint::Solid(Rgba::opaque(0, 0, 0)),
cap: LineCap::Round,
join: LineJoin::Bevel,
miter_limit: 8.0,
dash: Some(DashPattern {
array: vec![5.0, 3.0],
offset: 1.0,
}),
};
set_stroke_style(&mut op, &stroke, &mut res);
let s = String::from_utf8(op.into_bytes()).unwrap();
assert!(s.contains("2.5 w"));
assert!(s.contains("1 J"));
assert!(s.contains("2 j"));
assert!(s.contains("8 M"));
assert!(s.contains("[5 3] 1 d"));
}
#[test]
fn empty_path_writes_nothing() {
let p = Path::new();
let mut op = OpBuf::new();
emit_path(&mut op, &p);
assert!(op.as_bytes().is_empty());
let _node = PathNode {
path: Path::new(),
fill: None,
stroke: None,
fill_rule: FillRule::NonZero,
};
}
}