use crate::ffi::annot::ANNOTATIONS;
use crate::ffi::document::{Document, PAGES};
use crate::ffi::{DOCUMENTS, Handle, HandleStore};
use crate::fitz::geometry::Rect;
use crate::pdf::annot::{AnnotType, Annotation};
use std::sync::LazyLock;
type ContextHandle = Handle;
type DocumentHandle = Handle;
type PageHandle = Handle;
type AnnotHandle = Handle;
pub const PDF_REDACT_IMAGE_NONE: i32 = 0;
pub const PDF_REDACT_IMAGE_REMOVE: i32 = 1;
pub const PDF_REDACT_IMAGE_PIXELS: i32 = 2;
pub const PDF_REDACT_IMAGE_REMOVE_UNLESS_INVISIBLE: i32 = 3;
pub const PDF_REDACT_LINE_ART_NONE: i32 = 0;
pub const PDF_REDACT_LINE_ART_REMOVE_IF_COVERED: i32 = 1;
pub const PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED: i32 = 2;
pub const PDF_REDACT_TEXT_REMOVE: i32 = 0;
pub const PDF_REDACT_TEXT_NONE: i32 = 1;
pub const PDF_REDACT_TEXT_REMOVE_INVISIBLE: i32 = 2;
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RedactOptions {
pub black_boxes: i32,
pub image_method: i32,
pub line_art: i32,
pub text: i32,
}
impl Default for RedactOptions {
fn default() -> Self {
Self::new()
}
}
impl RedactOptions {
pub fn new() -> Self {
Self {
black_boxes: 1,
image_method: PDF_REDACT_IMAGE_REMOVE,
line_art: PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED,
text: PDF_REDACT_TEXT_REMOVE,
}
}
pub fn secure() -> Self {
Self {
black_boxes: 1,
image_method: PDF_REDACT_IMAGE_REMOVE,
line_art: PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED,
text: PDF_REDACT_TEXT_REMOVE,
}
}
pub fn ocr_only() -> Self {
Self {
black_boxes: 0,
image_method: PDF_REDACT_IMAGE_NONE,
line_art: PDF_REDACT_LINE_ART_NONE,
text: PDF_REDACT_TEXT_REMOVE_INVISIBLE,
}
}
pub fn preserve_visible() -> Self {
Self {
black_boxes: 0,
image_method: PDF_REDACT_IMAGE_PIXELS,
line_art: PDF_REDACT_LINE_ART_REMOVE_IF_COVERED,
text: PDF_REDACT_TEXT_REMOVE,
}
}
}
#[derive(Debug, Clone)]
pub struct RedactRegion {
pub rect: [f32; 4],
pub color: [f32; 3],
pub overlay_text: Option<String>,
pub applied: bool,
}
impl Default for RedactRegion {
fn default() -> Self {
Self::new()
}
}
impl RedactRegion {
pub fn new() -> Self {
Self {
rect: [0.0, 0.0, 0.0, 0.0],
color: [0.0, 0.0, 0.0], overlay_text: None,
applied: false,
}
}
pub fn with_rect(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
Self {
rect: [x0, y0, x1, y1],
color: [0.0, 0.0, 0.0],
overlay_text: None,
applied: false,
}
}
pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = [r, g, b];
self
}
pub fn with_overlay(mut self, text: &str) -> Self {
self.overlay_text = Some(text.to_string());
self
}
fn to_rect(&self) -> Rect {
Rect::new(self.rect[0], self.rect[1], self.rect[2], self.rect[3])
}
}
#[derive(Debug, Default)]
pub struct RedactContext {
pub regions: Vec<RedactRegion>,
pub document: DocumentHandle,
pub page: PageHandle,
pub options: RedactOptions,
pub stats: RedactStats,
}
impl RedactContext {
pub fn new(document: DocumentHandle, page: PageHandle) -> Self {
Self {
regions: Vec::new(),
document,
page,
options: RedactOptions::new(),
stats: RedactStats::default(),
}
}
pub fn add_region(&mut self, region: RedactRegion) {
self.regions.push(region);
}
pub fn clear_regions(&mut self) {
self.regions.clear();
}
pub fn apply(&mut self) -> i32 {
let unapplied: Vec<usize> = self
.regions
.iter()
.enumerate()
.filter(|(_, r)| !r.applied)
.map(|(i, _)| i)
.collect();
if unapplied.is_empty() {
return 0;
}
let redact_rects: Vec<Rect> = unapplied
.iter()
.map(|&i| self.regions[i].to_rect())
.collect();
let page_num = if let Some(page_arc) = PAGES.get(self.page) {
if let Ok(pg) = page_arc.lock() {
pg.page_num
} else {
0
}
} else {
0
};
if let Some(doc_arc) = DOCUMENTS.get(self.document) {
if let Ok(mut doc) = doc_arc.lock() {
let data = doc.data().to_vec();
if let Some(new_data) = redact_page_content(
&data,
page_num,
&redact_rects,
&self.options,
&mut self.stats,
) {
let final_data = if self.options.black_boxes != 0 {
let overlay_regions: Vec<&RedactRegion> =
unapplied.iter().map(|&i| &self.regions[i]).collect();
append_black_box_overlay(&new_data, page_num, &overlay_regions)
.unwrap_or(new_data)
} else {
new_data
};
doc.set_data(final_data);
}
}
}
let mut count = 0i32;
for &idx in &unapplied {
self.regions[idx].applied = true;
count += 1;
self.stats.regions_applied += 1;
}
count
}
}
#[derive(Debug, Default, Clone)]
#[repr(C)]
pub struct RedactStats {
pub regions_applied: i32,
pub text_removed: i32,
pub images_removed: i32,
pub images_modified: i32,
pub line_art_removed: i32,
pub annotations_removed: i32,
}
pub static REDACT_CONTEXTS: LazyLock<HandleStore<RedactContext>> = LazyLock::new(HandleStore::new);
fn find_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
(0..=data.len() - pattern.len()).find(|&i| &data[i..i + pattern.len()] == pattern)
}
fn rfind_pattern(data: &[u8], pattern: &[u8]) -> Option<usize> {
if pattern.is_empty() || data.len() < pattern.len() {
return None;
}
(0..=data.len() - pattern.len())
.rev()
.find(|&i| &data[i..i + pattern.len()] == pattern)
}
fn find_all_patterns(data: &[u8], pattern: &[u8]) -> Vec<usize> {
let mut positions = Vec::new();
if pattern.is_empty() || data.len() < pattern.len() {
return positions;
}
for i in 0..=data.len() - pattern.len() {
if &data[i..i + pattern.len()] == pattern {
positions.push(i);
}
}
positions
}
fn find_dict_end(data: &[u8], start: usize) -> Option<usize> {
if start + 1 >= data.len() || data[start] != b'<' || data[start + 1] != b'<' {
return None;
}
let mut depth = 0i32;
let mut i = start;
while i + 1 < data.len() {
if data[i] == b'<' && data[i + 1] == b'<' {
depth += 1;
i += 2;
} else if data[i] == b'>' && data[i + 1] == b'>' {
depth -= 1;
if depth == 0 {
return Some(i);
}
i += 2;
} else {
i += 1;
}
}
None
}
fn find_dict_key(data: &[u8], region_start: usize, region_end: usize, key: &[u8]) -> Option<usize> {
let end = region_end.min(data.len());
if region_start >= end {
return None;
}
let region = &data[region_start..end];
find_pattern(region, key).map(|pos| region_start + pos + key.len())
}
fn find_object_dict(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let pat_bytes = pattern.as_bytes();
let positions = find_all_patterns(data, pat_bytes);
for &pos in positions.iter().rev() {
let after = &data[pos..];
if let Some(dict_rel) = find_pattern(after, b"<<") {
let dict_start = pos + dict_rel;
if let Some(dict_end) = find_dict_end(data, dict_start) {
return Some((dict_start, dict_end + 2));
}
}
}
None
}
fn resolve_indirect_ref(data: &[u8], pos: usize) -> Option<i32> {
extract_int_after(data, pos)
}
fn extract_int_after(data: &[u8], pos: usize) -> Option<i32> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let negative = if i < data.len() && data[i] == b'-' {
i += 1;
true
} else {
false
};
let start = i;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
if i > start {
if let Ok(s) = std::str::from_utf8(&data[start..i]) {
if let Ok(n) = s.parse::<i32>() {
return Some(if negative { -n } else { n });
}
}
}
None
}
fn find_trailer_region(data: &[u8]) -> Option<(usize, usize)> {
let trailer_pos = rfind_pattern(data, b"trailer")?;
let after = &data[trailer_pos..];
let dict_start_rel = find_pattern(after, b"<<")?;
let dict_start = trailer_pos + dict_start_rel;
let dict_end = find_dict_end(data, dict_start)?;
Some((dict_start, dict_end + 2))
}
fn find_root_obj_num(data: &[u8]) -> Option<i32> {
let (ts, te) = find_trailer_region(data)?;
let kp = find_dict_key(data, ts, te, b"/Root")?;
resolve_indirect_ref(data, kp)
}
fn find_info_obj_num(data: &[u8]) -> Option<i32> {
let (ts, te) = find_trailer_region(data)?;
let kp = find_dict_key(data, ts, te, b"/Info")?;
resolve_indirect_ref(data, kp)
}
fn find_page_dict(data: &[u8], page_num: i32) -> Option<(usize, usize)> {
let pattern = b"/Type /Page";
let mut found = 0i32;
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern {
if data.get(i + pattern.len()) != Some(&b's') {
if found == page_num {
let search_start = i.saturating_sub(500);
let before = &data[search_start..i];
if let Some(obj_rel) = rfind_pattern(before, b" obj") {
let obj_abs = search_start + obj_rel;
let after_obj = &data[obj_abs..];
if let Some(dict_rel) = find_pattern(after_obj, b"<<") {
let dict_start = obj_abs + dict_rel;
if let Some(dict_end) = find_dict_end(data, dict_start) {
return Some((dict_start, dict_end + 2));
}
}
}
}
found += 1;
}
}
i += 1;
}
None
}
fn find_page_obj_num(data: &[u8], page_num: i32) -> Option<i32> {
let pattern = b"/Type /Page";
let mut found = 0i32;
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern && data.get(i + pattern.len()) != Some(&b's') {
if found == page_num {
let search_start = i.saturating_sub(500);
let before = &data[search_start..i];
if let Some(obj_rel) = rfind_pattern(before, b" 0 obj") {
let obj_pos = search_start + obj_rel;
let mut num_end = obj_pos;
let mut num_start = num_end;
while num_start > 0 && data[num_start - 1].is_ascii_digit() {
num_start -= 1;
}
if num_start < num_end {
if let Ok(s) = std::str::from_utf8(&data[num_start..num_end]) {
if let Ok(n) = s.parse::<i32>() {
return Some(n);
}
}
}
}
}
found += 1;
}
i += 1;
}
None
}
fn count_pages(data: &[u8]) -> i32 {
let pattern = b"/Type /Page";
let mut count = 0i32;
let mut i = 0;
while i + pattern.len() <= data.len() {
if &data[i..i + pattern.len()] == pattern && data.get(i + pattern.len()) != Some(&b's') {
count += 1;
}
i += 1;
}
count
}
fn find_stream_body(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let positions = find_all_patterns(data, pattern.as_bytes());
for &pos in positions.iter().rev() {
let remaining = &data[pos..];
if let Some(stream_kw) = find_pattern(remaining, b"stream") {
let abs_stream_kw = pos + stream_kw;
let mut body_start = abs_stream_kw + 6; if body_start < data.len() && data[body_start] == b'\r' {
body_start += 1;
}
if body_start < data.len() && data[body_start] == b'\n' {
body_start += 1;
}
let after_body = &data[body_start..];
if let Some(end_rel) = find_pattern(after_body, b"endstream") {
let mut body_end = body_start + end_rel;
if body_end > body_start && data[body_end - 1] == b'\n' {
body_end -= 1;
}
if body_end > body_start && data[body_end - 1] == b'\r' {
body_end -= 1;
}
return Some((body_start, body_end));
}
}
}
None
}
fn parse_float_token(token: &str) -> Option<f32> {
token.parse::<f32>().ok()
}
fn text_position_in_redact_regions(x: f32, y: f32, regions: &[Rect]) -> bool {
for r in regions {
if x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1 {
return true;
}
}
false
}
fn rect_fully_covers(outer: &Rect, inner: &Rect) -> bool {
outer.x0 <= inner.x0 && outer.y0 <= inner.y0 && outer.x1 >= inner.x1 && outer.y1 >= inner.y1
}
fn filter_content_stream(
stream: &[u8],
redact_rects: &[Rect],
options: &RedactOptions,
stats: &mut RedactStats,
) -> Vec<u8> {
let text = String::from_utf8_lossy(stream);
let mut output = Vec::new();
let mut in_text_block = false;
let mut suppress_text = false;
let mut current_x: f32 = 0.0;
let mut current_y: f32 = 0.0;
let mut in_path = false;
let mut path_buf: Vec<u8> = Vec::new();
let mut path_x_min: f32 = f32::MAX;
let mut path_y_min: f32 = f32::MAX;
let mut path_x_max: f32 = f32::MIN;
let mut path_y_max: f32 = f32::MIN;
for line in text.lines() {
let trimmed = line.trim();
if trimmed == "BT" {
in_text_block = true;
suppress_text = false;
current_x = 0.0;
current_y = 0.0;
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
if trimmed == "ET" {
in_text_block = false;
suppress_text = false;
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
if in_text_block {
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
if let Some(&op) = tokens.last() {
match op {
"Td" | "TD" if tokens.len() >= 3 => {
if let (Some(tx), Some(ty)) = (
parse_float_token(tokens[tokens.len() - 3]),
parse_float_token(tokens[tokens.len() - 2]),
) {
current_x += tx;
current_y += ty;
suppress_text = options.text != PDF_REDACT_TEXT_NONE
&& text_position_in_redact_regions(
current_x,
current_y,
redact_rects,
);
}
if !suppress_text {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
continue;
}
"Tm" if tokens.len() >= 7 => {
if let (Some(tx), Some(ty)) = (
parse_float_token(tokens[tokens.len() - 3]),
parse_float_token(tokens[tokens.len() - 2]),
) {
current_x = tx;
current_y = ty;
suppress_text = options.text != PDF_REDACT_TEXT_NONE
&& text_position_in_redact_regions(
current_x,
current_y,
redact_rects,
);
}
if !suppress_text {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
continue;
}
"Tj" | "TJ" | "'" | "\"" => {
if suppress_text && options.text != PDF_REDACT_TEXT_NONE {
stats.text_removed += 1;
continue; }
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
"Tr" if tokens.len() >= 2 => {
if options.text == PDF_REDACT_TEXT_REMOVE_INVISIBLE {
if let Some(mode) = parse_float_token(tokens[tokens.len() - 2]) {
if (mode - 3.0).abs() < 0.5 {
suppress_text = true;
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
}
}
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
_ => {
if !suppress_text {
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
continue;
}
}
}
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
if trimmed.ends_with(" Do") && options.image_method != PDF_REDACT_IMAGE_NONE {
let should_remove = options.image_method == PDF_REDACT_IMAGE_REMOVE
|| options.image_method == PDF_REDACT_IMAGE_REMOVE_UNLESS_INVISIBLE;
if should_remove {
stats.images_removed += 1;
continue;
}
if options.image_method == PDF_REDACT_IMAGE_PIXELS {
stats.images_modified += 1;
}
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
if options.line_art != PDF_REDACT_LINE_ART_NONE {
let tokens: Vec<&str> = trimmed.split_whitespace().collect();
if let Some(&op) = tokens.last() {
match op {
"m" | "l" | "c" | "v" | "y" | "re" => {
if !in_path {
in_path = true;
path_buf.clear();
path_x_min = f32::MAX;
path_y_min = f32::MAX;
path_x_max = f32::MIN;
path_y_max = f32::MIN;
}
for t in &tokens[..tokens.len() - 1] {
if let Some(v) = parse_float_token(t) {
if path_x_min == f32::MAX
|| (path_x_max != f32::MIN && path_y_min != f32::MAX)
{
if v < path_x_min {
path_x_min = v;
}
if v > path_x_max {
path_x_max = v;
}
} else {
if v < path_y_min {
path_y_min = v;
}
if v > path_y_max {
path_y_max = v;
}
}
}
}
if op == "re" && tokens.len() >= 5 {
if let (Some(rx), Some(ry), Some(rw), Some(rh)) = (
parse_float_token(tokens[0]),
parse_float_token(tokens[1]),
parse_float_token(tokens[2]),
parse_float_token(tokens[3]),
) {
path_x_min = path_x_min.min(rx);
path_y_min = path_y_min.min(ry);
path_x_max = path_x_max.max(rx + rw);
path_y_max = path_y_max.max(ry + rh);
}
}
path_buf.extend_from_slice(line.as_bytes());
path_buf.push(b'\n');
continue;
}
"S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" | "n" => {
if in_path {
in_path = false;
let path_rect =
Rect::new(path_x_min, path_y_min, path_x_max, path_y_max);
let should_remove = match options.line_art {
PDF_REDACT_LINE_ART_REMOVE_IF_COVERED => redact_rects
.iter()
.any(|r| rect_fully_covers(r, &path_rect)),
PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED => {
redact_rects.iter().any(|r| r.intersects(&path_rect))
}
_ => false,
};
if should_remove {
stats.line_art_removed += 1;
continue;
}
output.extend_from_slice(&path_buf);
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
continue;
}
_ => {}
}
}
}
output.extend_from_slice(line.as_bytes());
output.push(b'\n');
}
if in_path {
output.extend_from_slice(&path_buf);
}
output
}
fn redact_page_content(
data: &[u8],
page_num: i32,
redact_rects: &[Rect],
options: &RedactOptions,
stats: &mut RedactStats,
) -> Option<Vec<u8>> {
if redact_rects.is_empty() {
return None;
}
let (dict_start, dict_end) = find_page_dict(data, page_num)?;
let contents_pos = find_dict_key(data, dict_start, dict_end, b"/Contents")?;
let content_obj_num = resolve_indirect_ref(data, contents_pos)?;
let (body_start, body_end) = find_stream_body(data, content_obj_num)?;
let stream_bytes = &data[body_start..body_end];
let filtered = filter_content_stream(stream_bytes, redact_rects, options, stats);
let mut new_data = Vec::with_capacity(data.len());
new_data.extend_from_slice(&data[..body_start]);
new_data.extend_from_slice(&filtered);
new_data.extend_from_slice(&data[body_end..]);
if let Some((cdict_start, cdict_end)) = find_object_dict(data, content_obj_num) {
if cdict_start < body_start {
if let Some(len_pos) = find_dict_key(&new_data, cdict_start, cdict_end, b"/Length") {
let new_len_str = format!("{}", filtered.len());
let old_len_end = {
let mut j = len_pos;
while j < new_data.len() && new_data[j].is_ascii_whitespace() {
j += 1;
}
let start_j = j;
while j < new_data.len() && new_data[j].is_ascii_digit() {
j += 1;
}
if j > start_j { j } else { len_pos }
};
if old_len_end > len_pos {
let mut fixed = Vec::with_capacity(new_data.len());
fixed.extend_from_slice(&new_data[..len_pos]);
fixed.push(b' ');
fixed.extend_from_slice(new_len_str.as_bytes());
fixed.extend_from_slice(&new_data[old_len_end..]);
return Some(fixed);
}
}
}
}
Some(new_data)
}
fn append_black_box_overlay(
data: &[u8],
page_num: i32,
regions: &[&RedactRegion],
) -> Option<Vec<u8>> {
if regions.is_empty() {
return None;
}
let (dict_start, dict_end) = find_page_dict(data, page_num)?;
let contents_pos = find_dict_key(data, dict_start, dict_end, b"/Contents")?;
let content_obj_num = resolve_indirect_ref(data, contents_pos)?;
let (body_start, body_end) = find_stream_body(data, content_obj_num)?;
let mut overlay = Vec::new();
for region in regions {
let [x0, y0, x1, y1] = region.rect;
let w = x1 - x0;
let h = y1 - y0;
let [r, g, b] = region.color;
overlay.extend_from_slice(b"q\n");
overlay.extend_from_slice(format!("{} {} {} rg\n", r, g, b).as_bytes());
overlay.extend_from_slice(format!("{} {} {} {} re\n", x0, y0, w, h).as_bytes());
overlay.extend_from_slice(b"f\n");
if let Some(ref text) = region.overlay_text {
let font_size = 10.0f32;
let text_x = x0 + 2.0;
let text_y = y0 + 2.0;
let escaped = text
.replace('\\', "\\\\")
.replace('(', "\\(")
.replace(')', "\\)");
overlay.extend_from_slice(b"BT\n");
overlay.extend_from_slice(format!("/F1 {} Tf\n", font_size).as_bytes());
let luminance = 0.299 * r + 0.587 * g + 0.114 * b;
let text_color = if luminance < 0.5 { "1 1 1" } else { "0 0 0" };
overlay.extend_from_slice(format!("{} rg\n", text_color).as_bytes());
overlay.extend_from_slice(format!("{} {} Td\n", text_x, text_y).as_bytes());
overlay.extend_from_slice(format!("({}) Tj\n", escaped).as_bytes());
overlay.extend_from_slice(b"ET\n");
}
overlay.extend_from_slice(b"Q\n");
}
let existing_body = &data[body_start..body_end];
let mut new_body = Vec::with_capacity(existing_body.len() + overlay.len() + 1);
new_body.extend_from_slice(existing_body);
new_body.push(b'\n');
new_body.extend_from_slice(&overlay);
let mut new_data = Vec::with_capacity(data.len() + overlay.len());
new_data.extend_from_slice(&data[..body_start]);
new_data.extend_from_slice(&new_body);
new_data.extend_from_slice(&data[body_end..]);
if let Some((cdict_start, cdict_end)) = find_object_dict(data, content_obj_num) {
if cdict_start < body_start {
if let Some(len_pos) = find_dict_key(&new_data, cdict_start, cdict_end, b"/Length") {
let new_len_str = format!("{}", new_body.len());
let old_len_end = {
let mut j = len_pos;
while j < new_data.len() && new_data[j].is_ascii_whitespace() {
j += 1;
}
let start_j = j;
while j < new_data.len() && new_data[j].is_ascii_digit() {
j += 1;
}
if j > start_j { j } else { len_pos }
};
if old_len_end > len_pos {
let mut fixed = Vec::with_capacity(new_data.len());
fixed.extend_from_slice(&new_data[..len_pos]);
fixed.push(b' ');
fixed.extend_from_slice(new_len_str.as_bytes());
fixed.extend_from_slice(&new_data[old_len_end..]);
return Some(fixed);
}
}
}
}
Some(new_data)
}
fn remove_range(data: &[u8], start: usize, end: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len() - (end - start));
result.extend_from_slice(&data[..start]);
result.extend_from_slice(&data[end..]);
result
}
fn remove_dict_key(data: &[u8], dict_start: usize, dict_end: usize, key: &[u8]) -> Option<Vec<u8>> {
let end = dict_end.min(data.len());
if dict_start >= end {
return None;
}
let region = &data[dict_start..end];
let key_rel = find_pattern(region, key)?;
let key_abs = dict_start + key_rel;
let mut i = key_abs + key.len();
while i < end && data[i].is_ascii_whitespace() {
i += 1;
}
if i >= end {
return None;
}
match data[i] {
b'(' => {
let mut depth = 0i32;
while i < data.len() {
match data[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
b'\\' => i += 1, _ => {}
}
i += 1;
}
}
b'<' if i + 1 < data.len() && data[i + 1] == b'<' => {
if let Some(de) = find_dict_end(data, i) {
i = de + 2;
}
}
b'<' => {
while i < data.len() && data[i] != b'>' {
i += 1;
}
if i < data.len() {
i += 1;
}
}
b'[' => {
let mut depth = 1i32;
i += 1;
while i < data.len() && depth > 0 {
if data[i] == b'[' {
depth += 1;
} else if data[i] == b']' {
depth -= 1;
}
i += 1;
}
}
b'/' => {
i += 1;
while i < data.len()
&& !data[i].is_ascii_whitespace()
&& data[i] != b'/'
&& data[i] != b'>'
&& data[i] != b'<'
&& data[i] != b'['
&& data[i] != b']'
&& data[i] != b'('
{
i += 1;
}
}
_ => {
while i < data.len() && (data[i].is_ascii_digit() || data[i] == b'-' || data[i] == b'.')
{
i += 1;
}
let saved = i;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i].is_ascii_digit() {
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i] == b'R' {
i += 1; } else {
i = saved; }
} else {
i = saved;
}
}
}
Some(remove_range(data, key_abs, i))
}
fn nullify_object(data: &[u8], obj_num: i32) -> Option<Vec<u8>> {
let pattern = format!("{} 0 obj", obj_num);
let pat_bytes = pattern.as_bytes();
let positions = find_all_patterns(data, pat_bytes);
if positions.is_empty() {
return None;
}
let obj_start = *positions.last().unwrap();
let remaining = &data[obj_start..];
let endobj_rel = find_pattern(remaining, b"endobj")?;
let endobj_abs = obj_start + endobj_rel + 6;
let replacement = format!("{} 0 obj\n<< >>\nendobj", obj_num);
let mut new_data = Vec::with_capacity(data.len());
new_data.extend_from_slice(&data[..obj_start]);
new_data.extend_from_slice(replacement.as_bytes());
new_data.extend_from_slice(&data[endobj_abs..]);
Some(new_data)
}
fn remove_all_occurrences_of_key_in_all_dicts(data: &[u8], key: &[u8]) -> Vec<u8> {
let mut current = data.to_vec();
loop {
let positions = find_all_patterns(¤t, b"<<");
let mut changed = false;
for &pos in positions.iter().rev() {
if let Some(dict_end_inner) = find_dict_end(¤t, pos) {
let de = dict_end_inner + 2;
if let Some(new_data) = remove_dict_key(¤t, pos, de, key) {
current = new_data;
changed = true;
break; }
}
}
if !changed {
break;
}
}
current
}
fn collect_redact_annotations_for_page(page_handle: PageHandle) -> Vec<(Handle, Rect)> {
let mut result = Vec::new();
if let Some(page_arc) = PAGES.get(page_handle) {
if let Ok(page) = page_arc.lock() {
for &annot_handle in &page.annotations {
if let Some(annot_arc) = ANNOTATIONS.get(annot_handle) {
if let Ok(annot) = annot_arc.lock() {
if annot.annot_type() == AnnotType::Redact {
result.push((annot_handle, annot.rect()));
}
}
}
}
}
}
result
}
fn apply_single_annotation_redaction(annot_handle: AnnotHandle, opts: &RedactOptions) -> i32 {
let (rect, color_opt, overlay, page_handle, doc_handle) = {
let annot_arc = match ANNOTATIONS.get(annot_handle) {
Some(a) => a,
None => return 0,
};
let annot = match annot_arc.lock() {
Ok(a) => a,
Err(_) => return 0,
};
if annot.annot_type() != AnnotType::Redact {
return 0;
}
let r = annot.rect();
let c = annot.color();
let o = annot.contents().to_string();
let overlay_text = if o.is_empty() { None } else { Some(o) };
let mut found_page: PageHandle = 0;
let mut found_doc: DocumentHandle = 0;
for ph in PAGES.get_live_handles() {
if let Some(p_arc) = PAGES.get(ph) {
if let Ok(p) = p_arc.lock() {
if p.annotations.contains(&annot_handle) {
found_page = ph;
found_doc = p.doc_handle;
break;
}
}
}
}
(r, c, overlay_text, found_page, found_doc)
};
if page_handle == 0 || doc_handle == 0 {
return 0;
}
let page_num = if let Some(page_arc) = PAGES.get(page_handle) {
if let Ok(pg) = page_arc.lock() {
pg.page_num
} else {
return 0;
}
} else {
return 0;
};
let redact_rects = vec![rect];
let mut stats = RedactStats::default();
if let Some(doc_arc) = DOCUMENTS.get(doc_handle) {
if let Ok(mut doc) = doc_arc.lock() {
let data = doc.data().to_vec();
if let Some(new_data) =
redact_page_content(&data, page_num, &redact_rects, opts, &mut stats)
{
let final_data = if opts.black_boxes != 0 {
let region = RedactRegion {
rect: [rect.x0, rect.y0, rect.x1, rect.y1],
color: color_opt.unwrap_or([0.0, 0.0, 0.0]),
overlay_text: overlay,
applied: false,
};
let regions_ref: Vec<&RedactRegion> = vec![®ion];
append_black_box_overlay(&new_data, page_num, ®ions_ref).unwrap_or(new_data)
} else {
new_data
};
doc.set_data(final_data);
}
}
}
if let Some(page_arc) = PAGES.get(page_handle) {
if let Ok(mut pg) = page_arc.lock() {
pg.remove_annotation(annot_handle);
}
}
ANNOTATIONS.remove(annot_handle);
1
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_default_redact_options() -> RedactOptions {
RedactOptions::new()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_secure_redact_options() -> RedactOptions {
RedactOptions::secure()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_ocr_redact_options() -> RedactOptions {
RedactOptions::ocr_only()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_redact_context(
_ctx: ContextHandle,
doc: DocumentHandle,
page: PageHandle,
) -> Handle {
let context = RedactContext::new(doc, page);
REDACT_CONTEXTS.insert(context)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_redact_context(_ctx: ContextHandle, redact_ctx: Handle) {
REDACT_CONTEXTS.remove(redact_ctx);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_redact_options(
_ctx: ContextHandle,
redact_ctx: Handle,
opts: RedactOptions,
) {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let mut c = ctx_arc.lock().unwrap();
c.options = opts;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_add_redact_region(
_ctx: ContextHandle,
redact_ctx: Handle,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
) {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let mut c = ctx_arc.lock().unwrap();
c.add_region(RedactRegion::with_rect(x0, y0, x1, y1));
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_add_redact_region_with_color(
_ctx: ContextHandle,
redact_ctx: Handle,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
r: f32,
g: f32,
b: f32,
) {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let mut c = ctx_arc.lock().unwrap();
let region = RedactRegion::with_rect(x0, y0, x1, y1).with_color(r, g, b);
c.add_region(region);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_count_redact_regions(_ctx: ContextHandle, redact_ctx: Handle) -> i32 {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let c = ctx_arc.lock().unwrap();
return c.regions.len() as i32;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_clear_redact_regions(_ctx: ContextHandle, redact_ctx: Handle) {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let mut c = ctx_arc.lock().unwrap();
c.clear_regions();
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_apply_redactions(_ctx: ContextHandle, redact_ctx: Handle) -> i32 {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let mut c = ctx_arc.lock().unwrap();
return c.apply();
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_redact_page_annotations(
_ctx: ContextHandle,
doc: DocumentHandle,
page: PageHandle,
opts: *const RedactOptions,
) -> i32 {
let options = if opts.is_null() {
RedactOptions::new()
} else {
unsafe { *opts }
};
let redact_annots = collect_redact_annotations_for_page(page);
if redact_annots.is_empty() {
return 0;
}
let mut regions: Vec<RedactRegion> = Vec::new();
let mut annot_handles: Vec<Handle> = Vec::new();
for (handle, rect) in &redact_annots {
let mut region = RedactRegion::with_rect(rect.x0, rect.y0, rect.x1, rect.y1);
if let Some(a_arc) = ANNOTATIONS.get(*handle) {
if let Ok(a) = a_arc.lock() {
if let Some(color) = a.color() {
region.color = color;
}
let contents = a.contents();
if !contents.is_empty() {
region.overlay_text = Some(contents.to_string());
}
}
}
regions.push(region);
annot_handles.push(*handle);
}
let redact_rects: Vec<Rect> = redact_annots.iter().map(|(_, r)| *r).collect();
let page_num = if let Some(page_arc) = PAGES.get(page) {
if let Ok(pg) = page_arc.lock() {
pg.page_num
} else {
return 0;
}
} else {
return 0;
};
let mut stats = RedactStats::default();
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
if let Some(new_data) =
redact_page_content(&data, page_num, &redact_rects, &options, &mut stats)
{
let final_data = if options.black_boxes != 0 {
let region_refs: Vec<&RedactRegion> = regions.iter().collect();
append_black_box_overlay(&new_data, page_num, ®ion_refs).unwrap_or(new_data)
} else {
new_data
};
doc_guard.set_data(final_data);
}
}
}
let count = annot_handles.len() as i32;
if let Some(page_arc) = PAGES.get(page) {
if let Ok(mut pg) = page_arc.lock() {
for &ah in &annot_handles {
pg.remove_annotation(ah);
}
}
}
for &ah in &annot_handles {
ANNOTATIONS.remove(ah);
}
count
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_apply_redaction(
_ctx: ContextHandle,
annot: AnnotHandle,
opts: *const RedactOptions,
) -> i32 {
let options = if opts.is_null() {
RedactOptions::new()
} else {
unsafe { *opts }
};
apply_single_annotation_redaction(annot, &options)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_get_redact_stats(_ctx: ContextHandle, redact_ctx: Handle) -> RedactStats {
if let Some(ctx_arc) = REDACT_CONTEXTS.get(redact_ctx) {
let c = ctx_arc.lock().unwrap();
return c.stats.clone();
}
RedactStats::default()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_sanitize_metadata(_ctx: ContextHandle, doc: DocumentHandle) {
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let mut current = data;
if let Some(info_num) = find_info_obj_num(¤t) {
if let Some(new_data) = nullify_object(¤t, info_num) {
current = new_data;
}
}
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(meta_pos) = find_dict_key(¤t, ds, de, b"/Metadata") {
if let Some(meta_obj_num) = resolve_indirect_ref(¤t, meta_pos) {
if let Some(new_data) = nullify_object(¤t, meta_obj_num) {
current = new_data;
}
}
if let Some((ds2, de2)) = find_object_dict(¤t, root_num) {
if let Some(new_data) =
remove_dict_key(¤t, ds2, de2, b"/Metadata")
{
current = new_data;
}
}
}
}
}
if let Some((ts, te)) = find_trailer_region(¤t) {
if let Some(new_data) = remove_dict_key(¤t, ts, te, b"/ID") {
current = new_data;
}
}
doc_guard.set_data(current);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_metadata_field(
_ctx: ContextHandle,
doc: DocumentHandle,
field: *const std::ffi::c_char,
) {
if field.is_null() {
return;
}
let field_str = match crate::ffi::safe_helpers::c_str_to_str(field) {
Some(s) => s,
None => return,
};
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let pdf_key = format!("/{}", field_str);
let key_bytes = pdf_key.as_bytes();
if let Some(info_num) = find_info_obj_num(&data) {
if let Some((ds, de)) = find_object_dict(&data, info_num) {
if let Some(new_data) = remove_dict_key(&data, ds, de, key_bytes) {
doc_guard.set_data(new_data);
}
}
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_hidden_content(_ctx: ContextHandle, doc: DocumentHandle) {
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let mut current = data;
current = remove_all_occurrences_of_key_in_all_dicts(¤t, b"/JS");
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(names_pos) = find_dict_key(¤t, ds, de, b"/Names") {
if let Some(names_obj) = resolve_indirect_ref(¤t, names_pos) {
if let Some((ns, ne)) = find_object_dict(¤t, names_obj) {
if let Some(new_data) =
remove_dict_key(¤t, ns, ne, b"/JavaScript")
{
current = new_data;
}
if let Some((ns2, ne2)) = find_object_dict(¤t, names_obj) {
if let Some(new_data) =
remove_dict_key(¤t, ns2, ne2, b"/EmbeddedFiles")
{
current = new_data;
}
}
}
}
}
}
}
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(new_data) = remove_dict_key(¤t, ds, de, b"/OCProperties") {
current = new_data;
}
}
}
let page_count = count_pages(¤t);
for pn in 0..page_count {
if let Some((pds, pde)) = find_page_dict(¤t, pn) {
if let Some(new_data) = remove_dict_key(¤t, pds, pde, b"/Annots") {
current = new_data;
}
}
}
doc_guard.set_data(current);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_attachments(_ctx: ContextHandle, doc: DocumentHandle) {
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let mut current = data;
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(new_data) = remove_dict_key(¤t, ds, de, b"/AF") {
current = new_data;
}
}
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(names_pos) = find_dict_key(¤t, ds, de, b"/Names") {
if let Some(names_obj) = resolve_indirect_ref(¤t, names_pos) {
if let Some((ns, ne)) = find_object_dict(¤t, names_obj) {
if let Some(new_data) =
remove_dict_key(¤t, ns, ne, b"/EmbeddedFiles")
{
current = new_data;
}
}
}
}
}
}
doc_guard.set_data(current);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_javascript(_ctx: ContextHandle, doc: DocumentHandle) {
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let mut current = data;
current = remove_all_occurrences_of_key_in_all_dicts(¤t, b"/JS");
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(names_pos) = find_dict_key(¤t, ds, de, b"/Names") {
if let Some(names_obj) = resolve_indirect_ref(¤t, names_pos) {
if let Some((ns, ne)) = find_object_dict(¤t, names_obj) {
if let Some(new_data) =
remove_dict_key(¤t, ns, ne, b"/JavaScript")
{
current = new_data;
}
}
}
}
}
}
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(new_data) = remove_dict_key(¤t, ds, de, b"/AA") {
current = new_data;
}
}
}
if let Some(root_num) = find_root_obj_num(¤t) {
if let Some((ds, de)) = find_object_dict(¤t, root_num) {
if let Some(oa_pos) = find_dict_key(¤t, ds, de, b"/OpenAction") {
if let Some(oa_obj) = resolve_indirect_ref(¤t, oa_pos) {
if let Some((oas, oae)) = find_object_dict(¤t, oa_obj) {
if find_dict_key(¤t, oas, oae, b"/JS").is_some() {
if let Some((ds2, de2)) = find_object_dict(¤t, root_num) {
if let Some(new_data) =
remove_dict_key(¤t, ds2, de2, b"/OpenAction")
{
current = new_data;
}
}
}
}
}
}
}
}
doc_guard.set_data(current);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_comments(_ctx: ContextHandle, doc: DocumentHandle) {
if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(mut doc_guard) = doc_arc.lock() {
let data = doc_guard.data().to_vec();
let mut current = data;
let page_count = count_pages(¤t);
for pn in 0..page_count {
if let Some((pds, pde)) = find_page_dict(¤t, pn) {
if let Some(new_data) = remove_dict_key(¤t, pds, pde, b"/Annots") {
current = new_data;
}
}
}
doc_guard.set_data(current);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_create_redact_annot(
_ctx: ContextHandle,
page: PageHandle,
x0: f32,
y0: f32,
x1: f32,
y1: f32,
) -> AnnotHandle {
let rect = Rect::new(x0, y0, x1, y1);
let annot = Annotation::new(AnnotType::Redact, rect);
let handle = ANNOTATIONS.insert(annot);
if let Some(page_arc) = PAGES.get(page) {
if let Ok(mut pg) = page_arc.lock() {
pg.add_annotation(handle);
}
}
handle
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_redact_annot_color(
_ctx: ContextHandle,
annot: AnnotHandle,
r: f32,
g: f32,
b: f32,
) {
if let Some(a_arc) = ANNOTATIONS.get(annot) {
if let Ok(mut a) = a_arc.lock() {
a.set_color(Some([
r.clamp(0.0, 1.0),
g.clamp(0.0, 1.0),
b.clamp(0.0, 1.0),
]));
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_set_redact_annot_text(
_ctx: ContextHandle,
annot: AnnotHandle,
text: *const std::ffi::c_char,
) {
if text.is_null() {
return;
}
let text_str = match crate::ffi::safe_helpers::c_str_to_str(text) {
Some(s) => s,
None => return,
};
if let Some(a_arc) = ANNOTATIONS.get(annot) {
if let Ok(mut a) = a_arc.lock() {
a.set_contents(text_str);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_add_redact_annot_quad(
_ctx: ContextHandle,
annot: AnnotHandle,
quad: *const f32, ) {
if quad.is_null() {
return;
}
let points: [f32; 8] = unsafe {
[
*quad,
*quad.add(1),
*quad.add(2),
*quad.add(3),
*quad.add(4),
*quad.add(5),
*quad.add(6),
*quad.add(7),
]
};
let xs = [points[0], points[2], points[4], points[6]];
let ys = [points[1], points[3], points[5], points[7]];
let qx0 = xs.iter().cloned().fold(f32::MAX, f32::min);
let qy0 = ys.iter().cloned().fold(f32::MAX, f32::min);
let qx1 = xs.iter().cloned().fold(f32::MIN, f32::max);
let qy1 = ys.iter().cloned().fold(f32::MIN, f32::max);
if let Some(a_arc) = ANNOTATIONS.get(annot) {
if let Ok(mut a) = a_arc.lock() {
let current = a.rect();
let new_rect = Rect::new(
current.x0.min(qx0),
current.y0.min(qy0),
current.x1.max(qx1),
current.y1.max(qy1),
);
a.set_rect(new_rect);
let quad_str = format!(
"{},{},{},{},{},{},{},{}",
points[0],
points[1],
points[2],
points[3],
points[4],
points[5],
points[6],
points[7],
);
let existing = a.get_property("QuadPoints").unwrap_or("").to_string();
let new_quads = if existing.is_empty() {
quad_str
} else {
format!("{};{}", existing, quad_str)
};
a.set_property("QuadPoints".to_string(), new_quads);
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_redact_document(
_ctx: ContextHandle,
doc: DocumentHandle,
opts: *const RedactOptions,
) -> i32 {
let options = if opts.is_null() {
RedactOptions::new()
} else {
unsafe { *opts }
};
let page_count = if let Some(doc_arc) = DOCUMENTS.get(doc) {
if let Ok(doc_guard) = doc_arc.lock() {
doc_guard.page_count
} else {
return 0;
}
} else {
return 0;
};
let mut total = 0i32;
let page_handles: Vec<Handle> = PAGES
.get_live_handles()
.into_iter()
.filter(|&ph| {
if let Some(p_arc) = PAGES.get(ph) {
if let Ok(p) = p_arc.lock() {
return p.doc_handle == doc;
}
}
false
})
.collect();
for ph in page_handles {
total += pdf_redact_page_annotations(_ctx, doc, ph, &options);
}
let _ = page_count; total
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_apply_all_redactions(
_ctx: ContextHandle,
doc: DocumentHandle,
opts: *const RedactOptions,
) -> i32 {
pdf_redact_document(_ctx, doc, opts)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_options_default() {
let opts = RedactOptions::new();
assert_eq!(opts.black_boxes, 1);
assert_eq!(opts.image_method, PDF_REDACT_IMAGE_REMOVE);
assert_eq!(opts.line_art, PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED);
assert_eq!(opts.text, PDF_REDACT_TEXT_REMOVE);
}
#[test]
fn test_redact_options_secure() {
let opts = RedactOptions::secure();
assert_eq!(opts.black_boxes, 1);
assert_eq!(opts.image_method, PDF_REDACT_IMAGE_REMOVE);
assert_eq!(opts.text, PDF_REDACT_TEXT_REMOVE);
}
#[test]
fn test_redact_options_ocr() {
let opts = RedactOptions::ocr_only();
assert_eq!(opts.black_boxes, 0);
assert_eq!(opts.image_method, PDF_REDACT_IMAGE_NONE);
assert_eq!(opts.text, PDF_REDACT_TEXT_REMOVE_INVISIBLE);
}
#[test]
fn test_redact_region() {
let region = RedactRegion::with_rect(10.0, 20.0, 100.0, 50.0);
assert_eq!(region.rect, [10.0, 20.0, 100.0, 50.0]);
assert_eq!(region.color, [0.0, 0.0, 0.0]);
assert!(!region.applied);
}
#[test]
fn test_redact_region_with_color() {
let region = RedactRegion::with_rect(0.0, 0.0, 100.0, 100.0).with_color(1.0, 0.0, 0.0);
assert_eq!(region.color, [1.0, 0.0, 0.0]);
}
#[test]
fn test_redact_region_with_overlay() {
let region = RedactRegion::new().with_overlay("REDACTED");
assert_eq!(region.overlay_text, Some("REDACTED".to_string()));
}
#[test]
fn test_redact_context() {
let mut ctx = RedactContext::new(1, 1);
assert!(ctx.regions.is_empty());
ctx.add_region(RedactRegion::with_rect(0.0, 0.0, 50.0, 50.0));
ctx.add_region(RedactRegion::with_rect(50.0, 50.0, 100.0, 100.0));
assert_eq!(ctx.regions.len(), 2);
ctx.clear_regions();
assert!(ctx.regions.is_empty());
}
#[test]
fn test_redact_context_apply() {
let mut ctx = RedactContext::new(1, 1);
ctx.add_region(RedactRegion::with_rect(0.0, 0.0, 50.0, 50.0));
ctx.add_region(RedactRegion::with_rect(50.0, 50.0, 100.0, 100.0));
let applied = ctx.apply();
assert_eq!(applied, 2);
assert_eq!(ctx.stats.regions_applied, 2);
let applied2 = ctx.apply();
assert_eq!(applied2, 0);
}
#[test]
fn test_redact_stats() {
let stats = RedactStats::default();
assert_eq!(stats.regions_applied, 0);
assert_eq!(stats.text_removed, 0);
assert_eq!(stats.images_removed, 0);
}
#[test]
fn test_ffi_default_options() {
let opts = pdf_default_redact_options();
assert_eq!(opts.black_boxes, 1);
let secure = pdf_secure_redact_options();
assert_eq!(secure.text, PDF_REDACT_TEXT_REMOVE);
let ocr = pdf_ocr_redact_options();
assert_eq!(ocr.text, PDF_REDACT_TEXT_REMOVE_INVISIBLE);
}
#[test]
fn test_ffi_redact_context() {
let ctx = 0;
let doc = 1;
let page = 1;
let redact_ctx = pdf_new_redact_context(ctx, doc, page);
assert!(redact_ctx > 0);
pdf_add_redact_region(ctx, redact_ctx, 0.0, 0.0, 100.0, 100.0);
assert_eq!(pdf_count_redact_regions(ctx, redact_ctx), 1);
pdf_add_redact_region_with_color(ctx, redact_ctx, 100.0, 0.0, 200.0, 100.0, 1.0, 0.0, 0.0);
assert_eq!(pdf_count_redact_regions(ctx, redact_ctx), 2);
let applied = pdf_apply_redactions(ctx, redact_ctx);
assert_eq!(applied, 2);
let stats = pdf_get_redact_stats(ctx, redact_ctx);
assert_eq!(stats.regions_applied, 2);
pdf_clear_redact_regions(ctx, redact_ctx);
assert_eq!(pdf_count_redact_regions(ctx, redact_ctx), 0);
pdf_drop_redact_context(ctx, redact_ctx);
}
#[test]
fn test_ffi_set_options() {
let ctx = 0;
let redact_ctx = pdf_new_redact_context(ctx, 1, 1);
let opts = RedactOptions::ocr_only();
pdf_set_redact_options(ctx, redact_ctx, opts);
pdf_drop_redact_context(ctx, redact_ctx);
}
#[test]
fn test_image_method_constants() {
assert_eq!(PDF_REDACT_IMAGE_NONE, 0);
assert_eq!(PDF_REDACT_IMAGE_REMOVE, 1);
assert_eq!(PDF_REDACT_IMAGE_PIXELS, 2);
assert_eq!(PDF_REDACT_IMAGE_REMOVE_UNLESS_INVISIBLE, 3);
}
#[test]
fn test_line_art_constants() {
assert_eq!(PDF_REDACT_LINE_ART_NONE, 0);
assert_eq!(PDF_REDACT_LINE_ART_REMOVE_IF_COVERED, 1);
assert_eq!(PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED, 2);
}
#[test]
fn test_text_constants() {
assert_eq!(PDF_REDACT_TEXT_REMOVE, 0);
assert_eq!(PDF_REDACT_TEXT_NONE, 1);
assert_eq!(PDF_REDACT_TEXT_REMOVE_INVISIBLE, 2);
}
#[test]
fn test_filter_content_stream_text_removal() {
let stream = b"BT\n100 200 Td\n(Hello World) Tj\nET\n";
let rects = vec![Rect::new(0.0, 0.0, 200.0, 300.0)];
let opts = RedactOptions::new();
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert!(stats.text_removed > 0);
assert!(!text.contains("Hello World"));
}
#[test]
fn test_filter_content_stream_text_preserved() {
let stream = b"BT\n100 200 Td\n(Hello World) Tj\nET\n";
let rects = vec![Rect::new(500.0, 500.0, 600.0, 600.0)];
let opts = RedactOptions::new();
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.text_removed, 0);
assert!(text.contains("Hello World"));
}
#[test]
fn test_filter_content_stream_text_none_mode() {
let stream = b"BT\n100 200 Td\n(Hello World) Tj\nET\n";
let rects = vec![Rect::new(0.0, 0.0, 200.0, 300.0)];
let mut opts = RedactOptions::new();
opts.text = PDF_REDACT_TEXT_NONE;
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.text_removed, 0);
assert!(text.contains("Hello World"));
}
#[test]
fn test_filter_content_stream_image_removal() {
let stream = b"/Im1 Do\n";
let rects = vec![Rect::new(0.0, 0.0, 100.0, 100.0)];
let opts = RedactOptions::new();
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.images_removed, 1);
assert!(!text.contains("Do"));
}
#[test]
fn test_filter_content_stream_line_art_removal() {
let stream = b"10 10 80 80 re\nf\n";
let rects = vec![Rect::new(0.0, 0.0, 100.0, 100.0)];
let opts = RedactOptions::new();
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.line_art_removed, 1);
assert!(!text.contains("re"));
}
#[test]
fn test_remove_dict_key_basic() {
let data = b"<< /Type /Page /Title (Hello) /Count 5 >>";
let result = remove_dict_key(data, 0, data.len(), b"/Title");
assert!(result.is_some());
let new_data = result.unwrap();
let text = String::from_utf8_lossy(&new_data);
assert!(!text.contains("/Title"));
assert!(text.contains("/Type"));
assert!(text.contains("/Count"));
}
#[test]
fn test_remove_dict_key_indirect_ref() {
let data = b"<< /Info 5 0 R /Root 1 0 R >>";
let result = remove_dict_key(data, 0, data.len(), b"/Info");
assert!(result.is_some());
let new_data = result.unwrap();
let text = String::from_utf8_lossy(&new_data);
assert!(!text.contains("/Info"));
assert!(text.contains("/Root"));
}
#[test]
fn test_nullify_object() {
let data = b"5 0 obj\n<< /Title (Hello) >>\nendobj\n";
let result = nullify_object(data, 5);
assert!(result.is_some());
let new_data = result.unwrap();
let text = String::from_utf8_lossy(&new_data);
assert!(text.contains("5 0 obj"));
assert!(!text.contains("/Title"));
assert!(text.contains("endobj"));
}
#[test]
fn test_ffi_create_redact_annot() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 10.0, 20.0, 100.0, 50.0);
assert!(annot > 0);
if let Some(a_arc) = ANNOTATIONS.get(annot) {
let a = a_arc.lock().unwrap();
assert_eq!(a.annot_type(), AnnotType::Redact);
let r = a.rect();
assert_eq!(r.x0, 10.0);
assert_eq!(r.y0, 20.0);
assert_eq!(r.x1, 100.0);
assert_eq!(r.y1, 50.0);
}
if let Some(p_arc) = PAGES.get(page) {
let p = p_arc.lock().unwrap();
assert!(p.annotations.contains(&annot));
}
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_ffi_set_redact_annot_color() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 0.0, 0.0, 100.0, 100.0);
pdf_set_redact_annot_color(0, annot, 1.0, 0.5, 0.0);
if let Some(a_arc) = ANNOTATIONS.get(annot) {
let a = a_arc.lock().unwrap();
assert_eq!(a.color(), Some([1.0, 0.5, 0.0]));
}
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_ffi_set_redact_annot_text() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 0.0, 0.0, 100.0, 100.0);
let text = std::ffi::CString::new("REDACTED").unwrap();
pdf_set_redact_annot_text(0, annot, text.as_ptr());
if let Some(a_arc) = ANNOTATIONS.get(annot) {
let a = a_arc.lock().unwrap();
assert_eq!(a.contents(), "REDACTED");
}
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_ffi_add_redact_annot_quad() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 50.0, 50.0, 60.0, 60.0);
let quad: [f32; 8] = [10.0, 20.0, 100.0, 20.0, 100.0, 40.0, 10.0, 40.0];
pdf_add_redact_annot_quad(0, annot, quad.as_ptr());
if let Some(a_arc) = ANNOTATIONS.get(annot) {
let a = a_arc.lock().unwrap();
let r = a.rect();
assert!(r.x0 <= 10.0);
assert!(r.y0 <= 20.0);
assert!(r.x1 >= 100.0);
assert!(r.y1 >= 40.0);
assert!(a.get_property("QuadPoints").is_some());
}
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_region_to_rect() {
let region = RedactRegion::with_rect(10.0, 20.0, 100.0, 50.0);
let rect = region.to_rect();
assert_eq!(rect.x0, 10.0);
assert_eq!(rect.y0, 20.0);
assert_eq!(rect.x1, 100.0);
assert_eq!(rect.y1, 50.0);
}
#[test]
fn test_rect_fully_covers() {
let outer = Rect::new(0.0, 0.0, 100.0, 100.0);
let inner = Rect::new(10.0, 10.0, 50.0, 50.0);
assert!(rect_fully_covers(&outer, &inner));
assert!(!rect_fully_covers(&inner, &outer));
}
#[test]
fn test_text_position_in_redact_regions() {
let regions = vec![Rect::new(0.0, 0.0, 100.0, 100.0)];
assert!(text_position_in_redact_regions(50.0, 50.0, ®ions));
assert!(!text_position_in_redact_regions(150.0, 150.0, ®ions));
}
#[test]
fn test_redact_options_preserve_visible() {
let opts = RedactOptions::preserve_visible();
assert_eq!(opts.black_boxes, 0);
assert_eq!(opts.image_method, PDF_REDACT_IMAGE_PIXELS);
assert_eq!(opts.line_art, PDF_REDACT_LINE_ART_REMOVE_IF_COVERED);
}
#[test]
fn test_filter_content_stream_line_art_covered() {
let stream = b"10 10 80 80 re\nf\n";
let rects = vec![Rect::new(0.0, 0.0, 100.0, 100.0)];
let mut opts = RedactOptions::new();
opts.line_art = PDF_REDACT_LINE_ART_REMOVE_IF_COVERED;
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.line_art_removed, 1);
assert!(!text.contains("re"));
}
#[test]
fn test_filter_content_stream_tr_invisible() {
let stream = b"BT\n100 200 Td\n3 Tr\n(Secret) Tj\nET\n";
let rects = vec![Rect::new(0.0, 0.0, 200.0, 300.0)];
let mut opts = RedactOptions::ocr_only();
let mut stats = RedactStats::default();
let _filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
assert!(stats.text_removed > 0);
}
#[test]
fn test_filter_content_stream_image_pixels_mode() {
let stream = b"/Im1 Do\n";
let rects = vec![Rect::new(0.0, 0.0, 100.0, 100.0)];
let mut opts = RedactOptions::preserve_visible();
let mut stats = RedactStats::default();
let filtered = filter_content_stream(stream, &rects, &opts, &mut stats);
let text = String::from_utf8_lossy(&filtered);
assert_eq!(stats.images_modified, 1);
assert!(text.contains("Do"));
}
#[test]
fn test_internal_find_pattern() {
let data = b"<< /Type /Page >>";
assert!(find_pattern(data, b"/Type").is_some());
assert!(find_pattern(data, b"missing").is_none());
}
#[test]
fn test_internal_rfind_pattern() {
let data = b"foo bar foo";
assert_eq!(rfind_pattern(data, b"foo"), Some(8));
}
#[test]
fn test_internal_find_all_patterns() {
let data = b"aa bb aa cc aa";
let positions = find_all_patterns(data, b"aa");
assert_eq!(positions.len(), 3);
}
#[test]
fn test_internal_find_dict_end() {
let data = b"<< /Key /Value >>";
let result = find_dict_end(data, 0);
assert!(result.is_some());
}
#[test]
fn test_internal_extract_int_after() {
let data = b" 42 ";
assert_eq!(extract_int_after(data, 0), Some(42));
}
#[test]
fn test_ffi_set_redact_annot_text_null() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 0.0, 0.0, 100.0, 100.0);
pdf_set_redact_annot_text(0, annot, std::ptr::null());
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_ffi_add_redact_annot_quad_null() {
let page = PAGES.insert(crate::ffi::document::Page::new(0, 0));
let annot = pdf_create_redact_annot(0, page, 0.0, 0.0, 100.0, 100.0);
pdf_add_redact_annot_quad(0, annot, std::ptr::null());
ANNOTATIONS.remove(annot);
PAGES.remove(page);
}
#[test]
fn test_ffi_remove_metadata_field_null() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(
b"%PDF-1.4\n%%EOF".to_vec(),
));
pdf_remove_metadata_field(0, doc, std::ptr::null());
DOCUMENTS.remove(doc);
}
#[test]
fn test_ffi_apply_redaction_invalid_annot() {
assert_eq!(pdf_apply_redaction(0, 0, std::ptr::null()), 0);
}
#[test]
fn test_ffi_count_redact_regions_invalid_handle() {
assert_eq!(pdf_count_redact_regions(0, 0), 0);
}
#[test]
fn test_ffi_apply_redactions_invalid_handle() {
assert_eq!(pdf_apply_redactions(0, 0), 0);
}
#[test]
fn test_ffi_get_redact_stats_invalid_handle() {
let stats = pdf_get_redact_stats(0, 0);
assert_eq!(stats.regions_applied, 0);
}
#[test]
fn test_ffi_set_options_invalid_handle() {
pdf_set_redact_options(0, 0, RedactOptions::new());
}
#[test]
fn test_ffi_add_region_invalid_handle() {
pdf_add_redact_region(0, 0, 0.0, 0.0, 100.0, 100.0);
}
#[test]
fn test_ffi_clear_regions_invalid_handle() {
pdf_clear_redact_regions(0, 0);
}
#[test]
fn test_ffi_drop_redact_context_invalid() {
pdf_drop_redact_context(0, 0);
}
#[test]
fn test_ffi_add_redact_region_with_color_invalid_handle() {
pdf_add_redact_region_with_color(0, 0, 0.0, 0.0, 100.0, 100.0, 1.0, 0.0, 0.0);
}
#[test]
fn test_find_trailer_region() {
let data = b"trailer\n<< /Size 5 /Root 1 0 R /Info 2 0 R >>\nstartxref\n250\n%%EOF";
let result = find_trailer_region(data);
assert!(result.is_some());
}
#[test]
fn test_find_root_obj_num() {
let data = b"trailer\n<< /Size 5 /Root 1 0 R /Info 2 0 R >>\nstartxref\n250\n%%EOF";
assert_eq!(find_root_obj_num(data), Some(1));
}
#[test]
fn test_find_info_obj_num() {
let data = b"trailer\n<< /Size 5 /Root 1 0 R /Info 2 0 R >>\nstartxref\n250\n%%EOF";
assert_eq!(find_info_obj_num(data), Some(2));
}
#[test]
fn test_count_pages() {
let data = b"%PDF-1.4\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n4 0 obj\n<< /Type /Pages /Kids [3 0 R] >>\nendobj";
assert!(count_pages(data) >= 1);
}
#[test]
fn test_extract_int_after_negative() {
let data = b" -42 ";
assert_eq!(extract_int_after(data, 0), Some(-42));
}
#[test]
fn test_remove_range() {
let data = b"hello world";
let result = remove_range(data, 5, 11);
assert_eq!(result, b"hello");
}
#[test]
fn test_remove_dict_key_hex_string() {
let data = b"<< /Key <48656C6C6F> >>";
let result = remove_dict_key(data, 0, data.len(), b"/Key");
assert!(result.is_some());
}
#[test]
fn test_remove_dict_key_array() {
let data = b"<< /Key [1 2 3] >>";
let result = remove_dict_key(data, 0, data.len(), b"/Key");
assert!(result.is_some());
}
#[test]
fn test_remove_dict_key_name() {
let data = b"<< /Key /SomeName >>";
let result = remove_dict_key(data, 0, data.len(), b"/Key");
assert!(result.is_some());
}
#[test]
fn test_remove_all_occurrences_of_key() {
let data = b"<< /JS (script) >> << /JS (other) >>";
let result = remove_all_occurrences_of_key_in_all_dicts(data, b"/JS");
let text = String::from_utf8_lossy(&result);
assert!(!text.contains("/JS"));
}
#[test]
fn test_pdf_redact_page_annotations_null_opts() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(
b"%PDF-1.4\n%%EOF".to_vec(),
));
let page = PAGES.insert(crate::ffi::document::Page::new(doc, 0));
let count = pdf_redact_page_annotations(0, doc, page, std::ptr::null());
assert_eq!(count, 0);
PAGES.remove(page);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_redact_page_annotations_invalid_page() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(
b"%PDF-1.4\n%%EOF".to_vec(),
));
let count = pdf_redact_page_annotations(0, doc, 0, std::ptr::null());
assert_eq!(count, 0);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_redact_document() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(
b"%PDF-1.4\n%%EOF".to_vec(),
));
let count = pdf_redact_document(0, doc, std::ptr::null());
assert_eq!(count, 0);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_apply_all_redactions() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(
b"%PDF-1.4\n%%EOF".to_vec(),
));
let count = pdf_apply_all_redactions(0, doc, std::ptr::null());
assert_eq!(count, 0);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_sanitize_metadata() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R /Info 4 0 R /ID [<abc> <def>] >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
pdf_sanitize_metadata(0, doc);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_metadata_field_valid() {
let pdf = b"%PDF-1.4\n4 0 obj\n<< /Title (Doc) /Author (Me) >>\nendobj\ntrailer\n<< /Size 5 /Root 1 0 R /Info 4 0 R >>\nstartxref\n50\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
let field = std::ffi::CString::new("Title").unwrap();
pdf_remove_metadata_field(0, doc, field.as_ptr());
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_hidden_content() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
pdf_remove_hidden_content(0, doc);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_attachments() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
pdf_remove_attachments(0, doc);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_javascript() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
pdf_remove_javascript(0, doc);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_comments() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
pdf_remove_comments(0, doc);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_create_redact_annot_invalid_page() {
let annot = pdf_create_redact_annot(0, 0, 10.0, 20.0, 100.0, 50.0);
assert!(annot > 0);
ANNOTATIONS.remove(annot);
}
#[test]
fn test_pdf_set_redact_annot_color_invalid() {
pdf_set_redact_annot_color(0, 0, 1.0, 0.0, 0.0);
}
#[test]
fn test_find_page_dict() {
let data = b"3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj";
let result = find_page_dict(data, 0);
assert!(result.is_some());
}
#[test]
fn test_find_page_dict_not_found() {
let data = b"<< /Type /Pages >>";
let result = find_page_dict(data, 0);
assert!(result.is_none());
}
#[test]
fn test_find_page_obj_num() {
let data = b"3 0 obj\n<< /Type /Page >>\nendobj";
let result = find_page_obj_num(data, 0);
assert_eq!(result, Some(3));
}
#[test]
fn test_find_stream_body() {
let data = b"4 0 obj\n<< /Length 5 >>\nstream\nhello\nendstream\nendobj";
let result = find_stream_body(data, 4);
assert!(result.is_some());
let (start, end) = result.unwrap();
assert_eq!(&data[start..end], b"hello");
}
#[test]
fn test_find_object_dict() {
let data = b"5 0 obj\n<< /Title (Test) >>\nendobj";
let result = find_object_dict(data, 5);
assert!(result.is_some());
}
#[test]
fn test_find_dict_key() {
let data = b"<< /Type /Page /Contents 4 0 R >>";
let result = find_dict_key(data, 0, data.len(), b"/Contents");
assert!(result.is_some());
}
#[test]
fn test_resolve_indirect_ref() {
let data = b" 42 0 R";
let result = resolve_indirect_ref(data, 0);
assert_eq!(result, Some(42));
}
#[test]
fn test_parse_float_token() {
assert_eq!(parse_float_token("0.5"), Some(0.5));
assert_eq!(parse_float_token("invalid"), None);
}
#[test]
fn test_redact_page_content_empty_regions() {
let data = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj";
let rects: Vec<Rect> = vec![];
let opts = RedactOptions::new();
let mut stats = RedactStats::default();
let result = redact_page_content(data, 0, &rects, &opts, &mut stats);
assert!(result.is_none());
}
#[test]
fn test_append_black_box_overlay_empty() {
let data = b"%PDF-1.4\n";
let regions: Vec<&RedactRegion> = vec![];
let result = append_black_box_overlay(data, 0, ®ions);
assert!(result.is_none());
}
#[test]
fn test_redact_options_default_impl() {
let opts = RedactOptions::default();
assert_eq!(opts.black_boxes, 1);
}
#[test]
fn test_redact_region_default() {
let r = RedactRegion::default();
assert_eq!(r.rect, [0.0, 0.0, 0.0, 0.0]);
assert!(r.overlay_text.is_none());
}
#[test]
fn test_redact_context_apply_no_regions() {
let mut ctx = RedactContext::new(1, 1);
let applied = ctx.apply();
assert_eq!(applied, 0);
}
#[test]
fn test_pdf_redact_document_with_opts() {
let pdf = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n100\n%%EOF";
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(pdf.to_vec()));
let opts = RedactOptions::new();
let count = pdf_redact_document(0, doc, &opts);
DOCUMENTS.remove(doc);
assert!(count >= 0);
}
#[test]
fn test_pdf_redact_document_invalid_doc() {
let opts = RedactOptions::new();
let count = pdf_redact_document(0, 99999, &opts);
assert_eq!(count, 0);
}
#[test]
fn test_pdf_redact_document_null_opts() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R >> endobj\ntrailer << /Size 4 /Root 1 0 R >>\n%%EOF".to_vec()));
let count = pdf_redact_document(0, doc, std::ptr::null());
assert_eq!(count, 0);
DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_redact_document_valid_no_pages_loaded() {
let doc = DOCUMENTS.insert(crate::ffi::document::Document::new(b"%PDF-1.4\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj\n3 0 obj << /Type /Page /Parent 2 0 R >> endobj\ntrailer << /Size 4 /Root 1 0 R >>\n%%EOF".to_vec()));
let opts = RedactOptions::secure();
let count = pdf_redact_document(0, doc, &opts);
assert_eq!(count, 0);
DOCUMENTS.remove(doc);
}
}