use crate::annotation::{Annotation, AnnotationType};
pub fn generate_empty_ap(annotation: &Annotation) -> Vec<u8> {
let rect = &annotation.rect;
let w = (rect[2] - rect[0]).abs();
let h = (rect[3] - rect[1]).abs();
format!("1 g\n0 0 {w:.2} {h:.2} re\nf\n").into_bytes()
}
#[inline]
pub fn generate_annot_ap(annotation: &Annotation) -> Option<Vec<u8>> {
generate_annotation_appearance(annotation)
}
pub fn generate_annotation_appearance(annotation: &Annotation) -> Option<Vec<u8>> {
let rect = &annotation.rect;
let w = (rect[2] - rect[0]).abs();
let h = (rect[3] - rect[1]).abs();
if w <= 0.0 || h <= 0.0 {
return None;
}
match annotation.subtype {
AnnotationType::Text => Some(generate_text_note(w, h, annotation)),
AnnotationType::FreeText => generate_free_text(w, h, annotation),
AnnotationType::Highlight => generate_highlight(annotation),
AnnotationType::Underline => generate_underline(annotation),
AnnotationType::StrikeOut => generate_strikeout(annotation),
AnnotationType::Square => Some(generate_square(w, h, annotation)),
AnnotationType::Circle => Some(generate_circle(w, h, annotation)),
AnnotationType::Line => generate_line(annotation),
AnnotationType::Ink => generate_ink(annotation),
AnnotationType::Popup => Some(generate_popup(w, h, annotation)),
AnnotationType::Squiggly => generate_squiggly(annotation),
AnnotationType::Link => Some(generate_link(w, h, annotation)),
AnnotationType::Polygon => generate_polygon(annotation),
AnnotationType::PolyLine => generate_polyline(annotation),
AnnotationType::Stamp => Some(generate_stamp(w, h, annotation)),
AnnotationType::Caret => Some(generate_caret(w, h, annotation)),
AnnotationType::FileAttachment => Some(generate_file_attachment(w, h, annotation)),
AnnotationType::Sound => Some(generate_sound(w, h, annotation)),
AnnotationType::Movie => Some(generate_movie(w, h, annotation)),
AnnotationType::Screen => Some(generate_screen(w, h, annotation)),
AnnotationType::PrinterMark => Some(generate_printer_mark(w, h)),
AnnotationType::TrapNet => Some(generate_trap_net(w, h)),
AnnotationType::Watermark => Some(generate_watermark(w, h, annotation)),
AnnotationType::ThreeD => Some(generate_three_d(w, h, annotation)),
AnnotationType::RichMedia => Some(generate_rich_media(w, h, annotation)),
AnnotationType::XFAWidget => Some(generate_xfa_widget(w, h)),
AnnotationType::Redact => Some(generate_redact(w, h)),
_ => None,
}
}
fn color_rgb(annotation: &Annotation, default: &[f32]) -> (f32, f32, f32) {
let c = annotation.color.as_deref().unwrap_or(default);
match c.len() {
0 => (0.0, 0.0, 0.0),
1 => (c[0], c[0], c[0]),
3.. => (c[0], c[1], c[2]),
_ => (c[0], c[0], c[0]),
}
}
fn border_width(annotation: &Annotation) -> f32 {
annotation.border.as_ref().map(|b| b.width).unwrap_or(1.0)
}
fn generate_text_note(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[1.0, 1.0, 0.0]);
format!("q {r} {g} {b} rg 0 0 {w} {h} re f 0 0 0 RG 0 0 {w} {h} re S Q").into_bytes()
}
fn generate_free_text(_w: f32, h: f32, annotation: &Annotation) -> Option<Vec<u8>> {
let da = annotation.subtype_data.default_appearance.as_ref()?;
let text = annotation.contents.as_deref().unwrap_or("");
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let x = 2.0_f32;
let y = h - 12.0_f32;
let y = if y < 2.0 { 2.0 } else { y };
Some(format!("q BT {da} {x} {y} Td ({escaped}) Tj ET Q").into_bytes())
}
fn generate_highlight(annotation: &Annotation) -> Option<Vec<u8>> {
let quad_points = annotation.subtype_data.quad_points.as_ref()?;
let (r, g, b) = color_rgb(annotation, &[1.0, 1.0, 0.0]);
let mut stream = format!("q {r} {g} {b} rg ");
let mut offset = 0;
while offset + 7 < quad_points.len() {
let x0 = quad_points[offset];
let y0 = quad_points[offset + 1];
let x1 = quad_points[offset + 2];
let y1 = quad_points[offset + 3];
let x2 = quad_points[offset + 4];
let y2 = quad_points[offset + 5];
let x3 = quad_points[offset + 6];
let y3 = quad_points[offset + 7];
let rx = annotation.rect[0];
let ry = annotation.rect[1];
stream.push_str(&format!(
"{} {} m {} {} l {} {} l {} {} l f ",
x0 - rx,
y0 - ry,
x1 - rx,
y1 - ry,
x2 - rx,
y2 - ry,
x3 - rx,
y3 - ry
));
offset += 8;
}
stream.push('Q');
Some(stream.into_bytes())
}
fn generate_underline(annotation: &Annotation) -> Option<Vec<u8>> {
let quad_points = annotation.subtype_data.quad_points.as_ref()?;
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let mut stream = format!("q {r} {g} {b} RG 1 w ");
let mut offset = 0;
while offset + 7 < quad_points.len() {
let rx = annotation.rect[0];
let ry = annotation.rect[1];
let x0 = quad_points[offset] - rx;
let y0 = quad_points[offset + 1] - ry;
let x1 = quad_points[offset + 2] - rx;
let y1 = quad_points[offset + 3] - ry;
stream.push_str(&format!("{x0} {y0} m {x1} {y1} l S "));
offset += 8;
}
stream.push('Q');
Some(stream.into_bytes())
}
fn generate_strikeout(annotation: &Annotation) -> Option<Vec<u8>> {
let quad_points = annotation.subtype_data.quad_points.as_ref()?;
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let mut stream = format!("q {r} {g} {b} RG 1 w ");
let mut offset = 0;
while offset + 7 < quad_points.len() {
let rx = annotation.rect[0];
let ry = annotation.rect[1];
let y_bottom = (quad_points[offset + 1] + quad_points[offset + 3]) / 2.0;
let y_top = (quad_points[offset + 5] + quad_points[offset + 7]) / 2.0;
let y_mid = (y_bottom + y_top) / 2.0 - ry;
let x0 = quad_points[offset] - rx;
let x1 = quad_points[offset + 2] - rx;
stream.push_str(&format!("{x0} {y_mid} m {x1} {y_mid} l S "));
offset += 8;
}
stream.push('Q');
Some(stream.into_bytes())
}
fn generate_square(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let half = bw / 2.0;
format!(
"q {r} {g} {b} RG {bw} w {half} {half} {} {} re S Q",
w - bw,
h - bw
)
.into_bytes()
}
fn generate_circle(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let cx = w / 2.0;
let cy = h / 2.0;
let rx = cx - bw / 2.0;
let ry = cy - bw / 2.0;
let kx = rx * 0.5522847;
let ky = ry * 0.5522847;
format!(
"q {r} {g} {b} RG {bw} w \
{} {} m \
{} {} {} {} {} {} c \
{} {} {} {} {} {} c \
{} {} {} {} {} {} c \
{} {} {} {} {} {} c \
S Q",
cx + rx,
cy,
cx + rx,
cy + ky,
cx + kx,
cy + ry,
cx,
cy + ry,
cx - kx,
cy + ry,
cx - rx,
cy + ky,
cx - rx,
cy,
cx - rx,
cy - ky,
cx - kx,
cy - ry,
cx,
cy - ry,
cx + kx,
cy - ry,
cx + rx,
cy - ky,
cx + rx,
cy,
)
.into_bytes()
}
fn generate_ink(annotation: &Annotation) -> Option<Vec<u8>> {
let ink_list = annotation.subtype_data.ink_list.as_ref()?;
if ink_list.is_empty() {
return None;
}
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let rx = annotation.rect[0];
let ry = annotation.rect[1];
let mut stream = format!("q {r} {g} {b} RG {bw} w ");
for stroke in ink_list {
if stroke.len() < 2 {
continue;
}
let x0 = stroke[0] - rx;
let y0 = stroke[1] - ry;
stream.push_str(&format!("{x0} {y0} m "));
let mut i = 2;
while i + 1 < stroke.len() {
let x = stroke[i] - rx;
let y = stroke[i + 1] - ry;
stream.push_str(&format!("{x} {y} l "));
i += 2;
}
stream.push_str("S ");
}
stream.push('Q');
Some(stream.into_bytes())
}
fn generate_popup(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[1.0, 1.0, 0.75]);
let mut stream = format!("q {r} {g} {b} rg 0 0 {w} {h} re f 0 0 0 RG 0 0 {w} {h} re S ");
if let Some(ref text) = annotation.contents {
if !text.is_empty() {
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let font_size = 10.0_f32;
let x = 4.0_f32;
let y = h - font_size - 4.0;
let y = if y < 2.0 { 2.0 } else { y };
stream.push_str(&format!(
"BT /Helv {font_size} Tf {x} {y} Td ({escaped}) Tj ET "
));
}
}
stream.push('Q');
stream.into_bytes()
}
fn generate_squiggly(annotation: &Annotation) -> Option<Vec<u8>> {
let quad_points = annotation.subtype_data.quad_points.as_ref()?;
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let mut stream = format!("q {r} {g} {b} RG 0.5 w ");
let mut offset = 0;
while offset + 7 < quad_points.len() {
let rx = annotation.rect[0];
let ry = annotation.rect[1];
let x0 = quad_points[offset] - rx;
let y0 = quad_points[offset + 1] - ry;
let x1 = quad_points[offset + 2] - rx;
let y_base = y0;
let wave_height = 1.5_f32;
let wave_len = 4.0_f32;
let total_len = (x1 - x0).abs();
if total_len > 0.0 {
stream.push_str(&format!("{x0} {y_base} m "));
let num_waves = (total_len / wave_len).ceil() as usize;
let actual_wave_len = total_len / num_waves as f32;
let half = actual_wave_len / 2.0;
let mut cx = x0;
for i in 0..num_waves {
let up = if i % 2 == 0 {
wave_height
} else {
-wave_height
};
let mid_x = cx + half;
let end_x = cx + actual_wave_len;
let mid_y = y_base + up;
stream.push_str(&format!("{mid_x} {mid_y} {end_x} {y_base} v "));
cx = end_x;
}
stream.push_str("S ");
}
offset += 8;
}
stream.push('Q');
Some(stream.into_bytes())
}
fn generate_line(annotation: &Annotation) -> Option<Vec<u8>> {
let pts = annotation.subtype_data.line_points?;
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let rx = annotation.rect[0];
let ry = annotation.rect[1];
let x0 = pts[0] - rx;
let y0 = pts[1] - ry;
let x1 = pts[2] - rx;
let y1 = pts[3] - ry;
Some(format!("q {r} {g} {b} RG {bw} w {x0} {y0} m {x1} {y1} l S Q").into_bytes())
}
fn generate_link(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let bw = border_width(annotation);
if bw > 0.0 {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 1.0]);
format!("q {r} {g} {b} RG {bw} w 0 0 {w} {h} re S Q").into_bytes()
} else {
Vec::new() }
}
fn generate_polygon(annotation: &Annotation) -> Option<Vec<u8>> {
let vertices = annotation.subtype_data.vertices.as_ref()?;
if vertices.len() < 4 {
return None;
}
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let rect = &annotation.rect;
let ox = rect[0];
let oy = rect[1];
let mut stream = format!("q {r} {g} {b} RG {r} {g} {b} rg {bw} w ");
let x0 = vertices[0] - ox;
let y0 = vertices[1] - oy;
stream.push_str(&format!("{x0} {y0} m "));
let mut i = 2;
while i + 1 < vertices.len() {
let x = vertices[i] - ox;
let y = vertices[i + 1] - oy;
stream.push_str(&format!("{x} {y} l "));
i += 2;
}
stream.push_str("h B Q");
Some(stream.into_bytes())
}
fn generate_polyline(annotation: &Annotation) -> Option<Vec<u8>> {
let vertices = annotation.subtype_data.vertices.as_ref()?;
if vertices.len() < 4 {
return None;
}
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let rect = &annotation.rect;
let ox = rect[0];
let oy = rect[1];
let mut stream = format!("q {r} {g} {b} RG {bw} w ");
let x0 = vertices[0] - ox;
let y0 = vertices[1] - oy;
stream.push_str(&format!("{x0} {y0} m "));
let mut i = 2;
while i + 1 < vertices.len() {
let x = vertices[i] - ox;
let y = vertices[i + 1] - oy;
stream.push_str(&format!("{x} {y} l "));
i += 2;
}
stream.push_str("S Q");
Some(stream.into_bytes())
}
fn generate_stamp(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[1.0, 0.0, 0.0]);
let stamp_name = annotation
.subtype_data
.stamp_name
.as_deref()
.unwrap_or("Draft");
let escaped = stamp_name
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
let bw = border_width(annotation);
let text_y = h / 2.0 - 5.0;
let text_x = 4.0_f32;
format!(
"q {r} {g} {b} RG {bw} w 0 0 {w} {h} re S \
BT {r} {g} {b} rg /Helv 10 Tf {text_x} {text_y} Td ({escaped}) Tj ET Q"
)
.into_bytes()
}
fn generate_caret(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let mid_x = w / 2.0;
format!("q {r} {g} {b} RG {bw} w 0 0 m {mid_x} {h} l {w} 0 l S Q").into_bytes()
}
fn generate_file_attachment(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.5, 1.0]);
let bw = border_width(annotation);
let (cx, cy) = (w / 2.0, h / 2.0);
let icon_w = w * 0.5;
let icon_h = h * 0.7;
let x0 = cx - icon_w / 2.0;
let y0 = cy - icon_h / 2.0;
format!(
"q {r} {g} {b} RG {bw} w {x0} {y0} {icon_w} {icon_h} re S \
{bw2} w {x1} {y0} {icon_w2} {icon_h2} re S Q",
bw2 = bw * 0.5,
x1 = x0 + icon_w * 0.2,
icon_w2 = icon_w * 0.6,
icon_h2 = icon_h * 0.5,
)
.into_bytes()
}
fn generate_sound(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.5, 0.5, 0.0]);
let bw = border_width(annotation);
let cx = w * 0.35;
let cy = h / 2.0;
let tw = w * 0.25;
let th = h * 0.5;
format!(
"q {r} {g} {b} rg {bw} w \
{x0} {y0} m {x1} {cy} l {x0} {y2} l f \
{x0} {cy} m {x3} {cy} l S Q",
x0 = cx - tw,
y0 = cy - th / 2.0,
x1 = cx,
y2 = cy + th / 2.0,
x3 = cx + tw,
)
.into_bytes()
}
fn generate_movie(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.0, 0.0, 0.0]);
let bw = border_width(annotation);
let margin = w * 0.1;
format!(
"q {r} {g} {b} RG {bw} w {margin} {margin} {fw} {fh} re S \
{x1} {margin} m {x1} {top} l S \
{x2} {margin} m {x2} {top} l S Q",
fw = w - 2.0 * margin,
fh = h - 2.0 * margin,
top = h - margin,
x1 = w * 0.25,
x2 = w * 0.75,
)
.into_bytes()
}
fn generate_screen(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.2, 0.2, 0.2]);
let bw = border_width(annotation);
let margin = w * 0.05;
let label = annotation
.contents
.as_deref()
.unwrap_or("Screen")
.chars()
.take(8)
.collect::<String>();
let text_y = h * 0.4;
let text_x = margin + 2.0;
format!(
"q {r} {g} {b} RG {bw} w {margin} {margin} {fw} {fh} re S \
BT {r} {g} {b} rg /Helv 9 Tf {text_x} {text_y} Td ({label}) Tj ET Q",
fw = w - 2.0 * margin,
fh = h - 2.0 * margin,
)
.into_bytes()
}
fn generate_printer_mark(w: f32, h: f32) -> Vec<u8> {
let margin = 1.0_f32;
format!(
"q 0.5 0.5 0.5 RG 0.5 w {margin} {margin} {fw} {fh} re S Q",
fw = w - 2.0 * margin,
fh = h - 2.0 * margin,
)
.into_bytes()
}
fn generate_trap_net(w: f32, h: f32) -> Vec<u8> {
let margin = 1.0_f32;
format!(
"q 0.7 0.7 0.7 RG 0.5 w [2 2] 0 d {margin} {margin} {fw} {fh} re S Q",
fw = w - 2.0 * margin,
fh = h - 2.0 * margin,
)
.into_bytes()
}
fn generate_watermark(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let label = annotation
.subtype_data
.stamp_name
.as_deref()
.or(annotation.contents.as_deref())
.unwrap_or("WATERMARK");
let escaped = label
.chars()
.take(20)
.collect::<String>()
.replace('(', "\\(")
.replace(')', "\\)");
let font_size = (w / 8.0).clamp(8.0, 24.0);
let cx = w * 0.5;
let cy = h * 0.5;
format!(
"q 0.7 0.7 0.7 rg /GS1 gs \
BT /Helv {font_size} Tf {cx} {cy} Td 45 rotate ({escaped}) Tj ET Q"
)
.into_bytes()
}
fn generate_three_d(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.3, 0.3, 0.8]);
let bw = border_width(annotation);
let text_x = w * 0.3;
let text_y = h * 0.4;
format!(
"q {r} {g} {b} RG {bw} w 0 0 {w} {h} re S \
BT {r} {g} {b} rg /Helv 12 Tf {text_x} {text_y} Td (3D) Tj ET Q"
)
.into_bytes()
}
fn generate_rich_media(w: f32, h: f32, annotation: &Annotation) -> Vec<u8> {
let (r, g, b) = color_rgb(annotation, &[0.1, 0.1, 0.1]);
let bw = border_width(annotation);
let cx = w / 2.0;
let cy = h / 2.0;
let r2 = w.min(h) * 0.3;
let tx = cx - r2 * 0.3;
let ty1 = cy - r2 * 0.5;
let ty2 = cy + r2 * 0.5;
format!(
"q {r} {g} {b} RG {bw} w 0 0 {w} {h} re S \
{r} {g} {b} rg \
{tx} {ty1} m {tx2} {cy} l {tx} {ty2} l f Q",
tx2 = cx + r2 * 0.4,
)
.into_bytes()
}
fn generate_xfa_widget(w: f32, h: f32) -> Vec<u8> {
format!("q 0.8 0.8 0.8 RG 0.5 w 0 0 {w} {h} re S Q").into_bytes()
}
fn generate_redact(w: f32, h: f32) -> Vec<u8> {
format!("q 0 0 0 rg 0 0 {w} {h} re f Q").into_bytes()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::annotation::{
AnnotationBorder, AnnotationFlags, AnnotationSubtypeData, BorderStyle,
};
fn make_annotation(
subtype: AnnotationType,
rect: [f32; 4],
color: Option<Vec<f32>>,
border: Option<AnnotationBorder>,
subtype_data: AnnotationSubtypeData,
contents: Option<String>,
) -> Annotation {
Annotation {
subtype,
rect,
contents,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color,
border,
action: None,
destination: None,
subtype_data,
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
}
}
#[test]
fn test_generate_text_note_appearance() {
let annot = make_annotation(
AnnotationType::Text,
[0.0, 0.0, 24.0, 24.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("rg"));
assert!(s.contains("re f"));
assert!(s.contains("re S"));
}
#[test]
fn test_generate_free_text_appearance() {
let annot = make_annotation(
AnnotationType::FreeText,
[0.0, 0.0, 200.0, 50.0],
None,
None,
AnnotationSubtypeData {
default_appearance: Some("0 g /Helv 12 Tf".into()),
..Default::default()
},
Some("Hello World".into()),
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("BT"));
assert!(s.contains("Hello World"));
assert!(s.contains("Tj"));
}
#[test]
fn test_generate_highlight_appearance() {
let annot = make_annotation(
AnnotationType::Highlight,
[10.0, 700.0, 200.0, 720.0],
Some(vec![1.0, 1.0, 0.0]),
None,
AnnotationSubtypeData {
quad_points: Some(vec![10.0, 700.0, 200.0, 700.0, 200.0, 720.0, 10.0, 720.0]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("rg"));
assert!(s.contains(" f "));
}
#[test]
fn test_generate_underline_appearance() {
let annot = make_annotation(
AnnotationType::Underline,
[10.0, 700.0, 200.0, 720.0],
None,
None,
AnnotationSubtypeData {
quad_points: Some(vec![10.0, 700.0, 200.0, 700.0, 200.0, 720.0, 10.0, 720.0]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("RG"));
assert!(s.contains("l S"));
}
#[test]
fn test_generate_strikeout_appearance() {
let annot = make_annotation(
AnnotationType::StrikeOut,
[10.0, 700.0, 200.0, 720.0],
None,
None,
AnnotationSubtypeData {
quad_points: Some(vec![10.0, 700.0, 200.0, 700.0, 200.0, 720.0, 10.0, 720.0]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("RG"));
assert!(s.contains("l S"));
}
#[test]
fn test_generate_square_appearance() {
let annot = make_annotation(
AnnotationType::Square,
[0.0, 0.0, 100.0, 50.0],
Some(vec![1.0, 0.0, 0.0]),
Some(AnnotationBorder {
width: 2.0,
style: BorderStyle::Solid,
}),
AnnotationSubtypeData::default(),
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("re S"));
assert!(s.contains("2 w"));
}
#[test]
fn test_generate_circle_appearance() {
let annot = make_annotation(
AnnotationType::Circle,
[0.0, 0.0, 100.0, 100.0],
Some(vec![0.0, 0.0, 1.0]),
None,
AnnotationSubtypeData::default(),
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("c"));
assert!(s.contains("S Q"));
}
#[test]
fn test_generate_line_appearance() {
let annot = make_annotation(
AnnotationType::Line,
[10.0, 20.0, 100.0, 200.0],
None,
None,
AnnotationSubtypeData {
line_points: Some([10.0, 20.0, 100.0, 200.0]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("m"));
assert!(s.contains("l S"));
}
#[test]
fn test_generate_zero_size_returns_none() {
let annot = make_annotation(
AnnotationType::Text,
[10.0, 10.0, 10.0, 10.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
assert!(generate_annotation_appearance(&annot).is_none());
}
#[test]
fn test_generate_ink_single_stroke() {
let annot = make_annotation(
AnnotationType::Ink,
[0.0, 0.0, 100.0, 100.0],
None,
None,
AnnotationSubtypeData {
ink_list: Some(vec![vec![10.0, 10.0, 50.0, 50.0, 90.0, 10.0]]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("m "));
assert!(s.contains("l "));
assert!(s.contains("S "));
}
#[test]
fn test_generate_ink_multi_stroke() {
let annot = make_annotation(
AnnotationType::Ink,
[0.0, 0.0, 100.0, 100.0],
Some(vec![1.0, 0.0, 0.0]),
None,
AnnotationSubtypeData {
ink_list: Some(vec![
vec![10.0, 10.0, 50.0, 50.0],
vec![60.0, 60.0, 90.0, 90.0],
]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert_eq!(s.matches("S ").count(), 2);
}
#[test]
fn test_generate_ink_empty_list_returns_none() {
let annot = make_annotation(
AnnotationType::Ink,
[0.0, 0.0, 100.0, 100.0],
None,
None,
AnnotationSubtypeData {
ink_list: Some(vec![]),
..Default::default()
},
None,
);
assert!(generate_annotation_appearance(&annot).is_none());
}
#[test]
fn test_generate_ink_no_ink_list_returns_none() {
let annot = make_annotation(
AnnotationType::Ink,
[0.0, 0.0, 100.0, 100.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
assert!(generate_annotation_appearance(&annot).is_none());
}
#[test]
fn test_generate_popup_with_content() {
let annot = make_annotation(
AnnotationType::Popup,
[0.0, 0.0, 200.0, 100.0],
None,
None,
AnnotationSubtypeData::default(),
Some("A comment".into()),
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("rg"));
assert!(s.contains("re f"));
assert!(s.contains("BT"));
assert!(s.contains("A comment"));
assert!(s.contains("Tj"));
}
#[test]
fn test_generate_popup_without_content() {
let annot = make_annotation(
AnnotationType::Popup,
[0.0, 0.0, 200.0, 100.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("re f"));
assert!(!s.contains("BT"));
}
#[test]
fn test_generate_squiggly_appearance() {
let annot = make_annotation(
AnnotationType::Squiggly,
[10.0, 700.0, 200.0, 720.0],
Some(vec![0.0, 0.5, 0.0]),
None,
AnnotationSubtypeData {
quad_points: Some(vec![10.0, 700.0, 200.0, 700.0, 200.0, 720.0, 10.0, 720.0]),
..Default::default()
},
None,
);
let ap = generate_annotation_appearance(&annot).unwrap();
let s = String::from_utf8(ap).unwrap();
assert!(s.contains("RG"));
assert!(s.contains("m "));
assert!(s.contains("v "));
assert!(s.contains("S "));
}
#[test]
fn test_generate_squiggly_no_quad_points_returns_none() {
let annot = make_annotation(
AnnotationType::Squiggly,
[0.0, 0.0, 100.0, 20.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
assert!(generate_annotation_appearance(&annot).is_none());
}
#[test]
fn test_generate_unsupported_subtype_returns_none() {
let annot = make_annotation(
AnnotationType::Widget,
[0.0, 0.0, 100.0, 100.0],
None,
None,
AnnotationSubtypeData::default(),
None,
);
assert!(generate_annotation_appearance(&annot).is_none());
}
#[test]
fn test_generate_link_with_border() {
let annot = Annotation {
subtype: AnnotationType::Link,
rect: [0.0, 0.0, 100.0, 20.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: Some(vec![0.0, 0.0, 1.0]),
border: Some(AnnotationBorder {
width: 1.0,
style: BorderStyle::Solid,
}),
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S"));
}
#[test]
fn test_generate_polygon_triangle() {
let annot = Annotation {
subtype: AnnotationType::Polygon,
rect: [0.0, 0.0, 100.0, 100.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: Some(vec![1.0, 0.0, 0.0]),
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData {
vertices: Some(vec![0.0, 0.0, 50.0, 100.0, 100.0, 0.0]),
..Default::default()
},
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("m "));
assert!(s.contains("h B Q"));
}
#[test]
fn test_generate_polyline_path() {
let annot = Annotation {
subtype: AnnotationType::PolyLine,
rect: [10.0, 10.0, 30.0, 20.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData {
vertices: Some(vec![10.0, 10.0, 20.0, 20.0, 30.0, 10.0]),
..Default::default()
},
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("S Q"));
assert!(!s.contains("h B")); }
#[test]
fn test_generate_stamp_default() {
let annot = Annotation {
subtype: AnnotationType::Stamp,
rect: [0.0, 0.0, 150.0, 80.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("Draft")); }
#[test]
fn test_generate_stamp_named() {
let annot = Annotation {
subtype: AnnotationType::Stamp,
rect: [0.0, 0.0, 150.0, 80.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: Some(vec![0.0, 0.5, 0.0]),
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData {
stamp_name: Some("Approved".into()),
..Default::default()
},
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("Approved"));
}
#[test]
fn test_generate_caret_appearance() {
let annot = Annotation {
subtype: AnnotationType::Caret,
rect: [0.0, 0.0, 20.0, 15.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("m ")); assert!(s.contains("l ")); assert!(s.contains("S Q")); }
#[test]
fn test_polygon_no_vertices_returns_none() {
let annot = Annotation {
subtype: AnnotationType::Polygon,
rect: [0.0, 0.0, 100.0, 100.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
};
assert!(generate_annotation_appearance(&annot).is_none());
}
fn simple_annot(subtype: AnnotationType) -> Annotation {
Annotation {
subtype,
rect: [0.0, 0.0, 100.0, 50.0],
contents: None,
flags: AnnotationFlags::from_bits(0),
name: None,
appearance: None,
color: None,
border: None,
action: None,
destination: None,
subtype_data: AnnotationSubtypeData::default(),
mk: None,
file_spec: None,
parent_ref: None,
object_id: None,
open: None,
ap_n_bytes: None,
ap_r_bytes: None,
ap_d_bytes: None,
irt_ref: None,
field_name: None,
alternate_name: None,
field_value: None,
form_field_flags: None,
}
}
#[test]
fn test_file_attachment_generates_ap() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::FileAttachment));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S")); }
#[test]
fn test_sound_generates_ap() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::Sound));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("f")); }
#[test]
fn test_movie_generates_ap() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::Movie));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S")); }
#[test]
fn test_screen_generates_ap_with_label() {
let mut annot = simple_annot(AnnotationType::Screen);
annot.contents = Some("MyScreen".to_string());
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("MyScreen"));
}
#[test]
fn test_printer_mark_generates_empty_border() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::PrinterMark));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S")); }
#[test]
fn test_trap_net_generates_dashed_border() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::TrapNet));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("[2 2]")); }
#[test]
fn test_watermark_generates_ap_with_default_label() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::Watermark));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("WATERMARK"));
}
#[test]
fn test_watermark_uses_stamp_name() {
let mut annot = simple_annot(AnnotationType::Watermark);
annot.subtype_data.stamp_name = Some("DRAFT".to_string());
let ap = generate_annotation_appearance(&annot);
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("DRAFT"));
}
#[test]
fn test_three_d_generates_ap_with_label() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::ThreeD));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("3D"));
}
#[test]
fn test_rich_media_generates_ap_with_play_button() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::RichMedia));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S")); assert!(s.contains("m ")); }
#[test]
fn test_xfa_widget_generates_placeholder() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::XFAWidget));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("re S"));
}
#[test]
fn test_redact_generates_black_fill() {
let ap = generate_annotation_appearance(&simple_annot(AnnotationType::Redact));
assert!(ap.is_some());
let bytes = ap.unwrap();
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("0 0 0 rg")); assert!(s.contains("re f")); }
#[test]
fn test_generate_empty_ap_produces_white_fill() {
let annot = simple_annot(AnnotationType::Text);
let bytes = generate_empty_ap(&annot);
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("1 g")); assert!(s.contains("re\nf\n")); }
#[test]
fn test_generate_empty_ap_uses_annotation_rect_dimensions() {
let annot = simple_annot(AnnotationType::Square);
let bytes = generate_empty_ap(&annot);
let s = String::from_utf8_lossy(&bytes);
assert!(s.contains("100.00"));
assert!(s.contains("50.00"));
}
#[test]
fn test_generate_annot_ap_delegates_to_annotation_appearance() {
let annot = simple_annot(AnnotationType::Square);
let via_alias = generate_annot_ap(&annot);
let direct = generate_annotation_appearance(&annot);
assert_eq!(via_alias, direct);
}
}