use crate::ffi::{Handle, HandleStore};
use std::ffi::c_void;
use std::sync::LazyLock;
type ContextHandle = Handle;
type DocumentHandle = Handle;
type ColorspaceHandle = Handle;
type ShadeHandle = Handle;
pub const RECOLOR_GRAY: i32 = 1;
pub const RECOLOR_RGB: i32 = 3;
pub const RECOLOR_CMYK: i32 = 4;
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RecolorOptions {
pub num_comp: i32,
}
impl Default for RecolorOptions {
fn default() -> Self {
Self::rgb()
}
}
impl RecolorOptions {
pub fn gray() -> Self {
Self {
num_comp: RECOLOR_GRAY,
}
}
pub fn rgb() -> Self {
Self {
num_comp: RECOLOR_RGB,
}
}
pub fn cmyk() -> Self {
Self {
num_comp: RECOLOR_CMYK,
}
}
pub fn is_valid(&self) -> bool {
matches!(self.num_comp, 1 | 3 | 4)
}
}
#[derive(Debug, Clone)]
pub struct RecolorVertex {
pub src_color: Vec<f32>,
pub dst_color: Vec<f32>,
pub src_cs: ColorspaceHandle,
pub dst_cs: ColorspaceHandle,
}
impl RecolorVertex {
pub fn new(src_components: usize, dst_components: usize) -> Self {
Self {
src_color: vec![0.0; src_components],
dst_color: vec![0.0; dst_components],
src_cs: 0,
dst_cs: 0,
}
}
}
pub type RecolorVertexFn = extern "C" fn(
ctx: ContextHandle,
opaque: *mut c_void,
dst_cs: ColorspaceHandle,
dst: *mut f32,
src_cs: ColorspaceHandle,
src: *const f32,
);
pub type ShadeRecolorerFn = extern "C" fn(
ctx: ContextHandle,
opaque: *mut c_void,
src_cs: ColorspaceHandle,
dst_cs: *mut ColorspaceHandle,
) -> *const RecolorVertexFn;
#[derive(Debug)]
pub struct ShadeRecolorContext {
pub src_colorspace: ColorspaceHandle,
pub dst_colorspace: ColorspaceHandle,
pub opaque: *mut c_void,
pub vertex_fn: Option<RecolorVertexFn>,
pub shade_fn: Option<ShadeRecolorerFn>,
}
unsafe impl Send for ShadeRecolorContext {}
unsafe impl Sync for ShadeRecolorContext {}
impl Default for ShadeRecolorContext {
fn default() -> Self {
Self::new()
}
}
impl ShadeRecolorContext {
pub fn new() -> Self {
Self {
src_colorspace: 0,
dst_colorspace: 0,
opaque: std::ptr::null_mut(),
vertex_fn: None,
shade_fn: None,
}
}
pub fn with_colorspaces(src: ColorspaceHandle, dst: ColorspaceHandle) -> Self {
Self {
src_colorspace: src,
dst_colorspace: dst,
opaque: std::ptr::null_mut(),
vertex_fn: None,
shade_fn: None,
}
}
}
#[derive(Debug, Default, Clone)]
#[repr(C)]
pub struct RecolorStats {
pub pages_processed: i32,
pub colors_converted: i32,
pub shades_recolored: i32,
pub images_processed: i32,
pub output_intents_removed: i32,
}
pub static SHADE_RECOLOR_CONTEXTS: LazyLock<HandleStore<ShadeRecolorContext>> =
LazyLock::new(HandleStore::new);
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_options_gray() -> RecolorOptions {
RecolorOptions::gray()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_options_rgb() -> RecolorOptions {
RecolorOptions::rgb()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_options_cmyk() -> RecolorOptions {
RecolorOptions::cmyk()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_options_new(num_comp: i32) -> RecolorOptions {
RecolorOptions { num_comp }
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_options_is_valid(opts: *const RecolorOptions) -> i32 {
if opts.is_null() {
return 0;
}
unsafe { if (*opts).is_valid() { 1 } else { 0 } }
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_page(
_ctx: ContextHandle,
doc: DocumentHandle,
pagenum: i32,
opts: *const RecolorOptions,
) {
if opts.is_null() {
return;
}
let target = unsafe { &*opts };
if !target.is_valid() {
return;
}
let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
Some(d) => d,
None => return,
};
let mut guard = match doc_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let data = guard.data().to_vec();
if let Some(new_data) = recolor_page_in_data(&data, pagenum, target.num_comp) {
guard.set_data(new_data);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_document(
_ctx: ContextHandle,
doc: DocumentHandle,
opts: *const RecolorOptions,
) -> RecolorStats {
if opts.is_null() {
return RecolorStats::default();
}
let target = unsafe { &*opts };
if !target.is_valid() {
return RecolorStats::default();
}
let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
Some(d) => d,
None => return RecolorStats::default(),
};
let mut guard = match doc_arc.lock() {
Ok(g) => g,
Err(_) => return RecolorStats::default(),
};
let page_count = guard.page_count;
let mut stats = RecolorStats::default();
let mut current_data = guard.data().to_vec();
for page in 0..page_count {
if let Some(new_data) = recolor_page_in_data(¤t_data, page, target.num_comp) {
current_data = new_data;
stats.pages_processed += 1;
stats.colors_converted += 1; }
}
if stats.pages_processed > 0 {
guard.set_data(current_data);
}
stats
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_pages(
_ctx: ContextHandle,
doc: DocumentHandle,
start_page: i32,
end_page: i32,
opts: *const RecolorOptions,
) -> RecolorStats {
if opts.is_null() {
return RecolorStats::default();
}
let target = unsafe { &*opts };
if !target.is_valid() {
return RecolorStats::default();
}
let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
Some(d) => d,
None => return RecolorStats::default(),
};
let mut guard = match doc_arc.lock() {
Ok(g) => g,
Err(_) => return RecolorStats::default(),
};
let page_count = guard.page_count;
let start = start_page.max(0);
let end = end_page.min(page_count - 1);
let mut stats = RecolorStats::default();
if start > end {
return stats;
}
let mut current_data = guard.data().to_vec();
for page in start..=end {
if let Some(new_data) = recolor_page_in_data(¤t_data, page, target.num_comp) {
current_data = new_data;
stats.pages_processed += 1;
stats.colors_converted += 1;
}
}
if stats.pages_processed > 0 {
guard.set_data(current_data);
}
stats
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_remove_output_intents(_ctx: ContextHandle, doc: DocumentHandle) {
let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
Some(d) => d,
None => return,
};
let mut guard = match doc_arc.lock() {
Ok(g) => g,
Err(_) => return,
};
let data = guard.data().to_vec();
let text = String::from_utf8_lossy(&data);
let oi_key = "/OutputIntents";
let oi_pos = match text.find(oi_key) {
Some(p) => p,
None => return,
};
let after_key = oi_pos + oi_key.len();
let after_text = &data[after_key..];
let mut value_start = 0;
while value_start < after_text.len() && after_text[value_start].is_ascii_whitespace() {
value_start += 1;
}
if value_start >= after_text.len() {
return;
}
let value_end = if after_text[value_start] == b'[' {
let mut depth = 0i32;
let mut i = value_start;
loop {
if i >= after_text.len() {
break i;
}
match after_text[i] {
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
break i + 1;
}
}
_ => {}
}
i += 1;
}
} else if after_text[value_start].is_ascii_digit() {
let mut i = value_start;
while i < after_text.len() && after_text[i].is_ascii_digit() {
i += 1;
}
while i < after_text.len() && after_text[i].is_ascii_whitespace() {
i += 1;
}
while i < after_text.len() && after_text[i].is_ascii_digit() {
i += 1;
}
while i < after_text.len() && after_text[i].is_ascii_whitespace() {
i += 1;
}
if i < after_text.len() && after_text[i] == b'R' {
i += 1;
}
i
} else {
return;
};
let remove_start = oi_pos;
let remove_end = after_key + value_end;
let mut new_data = Vec::with_capacity(data.len() - (remove_end - remove_start));
new_data.extend_from_slice(&data[..remove_start]);
new_data.extend_from_slice(&data[remove_end..]);
guard.set_data(new_data);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_count_output_intents(_ctx: ContextHandle, doc: DocumentHandle) -> i32 {
let doc_arc = match crate::ffi::DOCUMENTS.get(doc) {
Some(d) => d,
None => return 0,
};
let guard = match doc_arc.lock() {
Ok(g) => g,
Err(_) => return 0,
};
let data = guard.data();
let text = String::from_utf8_lossy(data);
let oi_pos = match text.find("/OutputIntents") {
Some(p) => p,
None => return 0,
};
let after = &text[oi_pos + 14..];
let arr_start = match after.find('[') {
Some(p) => p,
None => return 0,
};
let arr_text = &after[arr_start..];
let arr_end = match arr_text.find(']') {
Some(p) => p,
None => return 0,
};
let array_content = &arr_text[1..arr_end];
let mut count = 0i32;
let mut depth = 0i32;
let bytes = array_content.as_bytes();
let mut i = 0;
while i + 1 < bytes.len() {
if bytes[i] == b'<' && bytes[i + 1] == b'<' {
if depth == 0 {
count += 1;
}
depth += 1;
i += 2;
} else if bytes[i] == b'>' && bytes[i + 1] == b'>' {
depth -= 1;
i += 2;
} else if bytes[i].is_ascii_digit() && depth == 0 {
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && bytes[j] == b'R' {
count += 1;
i = j + 1;
} else {
i += 1;
}
} else {
i += 1;
}
}
count
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_shade_recolor_context(
_ctx: ContextHandle,
src_cs: ColorspaceHandle,
dst_cs: ColorspaceHandle,
) -> Handle {
let context = ShadeRecolorContext::with_colorspaces(src_cs, dst_cs);
SHADE_RECOLOR_CONTEXTS.insert(context)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_shade_recolor_context(_ctx: ContextHandle, recolor_ctx: Handle) {
SHADE_RECOLOR_CONTEXTS.remove(recolor_ctx);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_shade_recolor_set_opaque(
_ctx: ContextHandle,
recolor_ctx: Handle,
opaque: *mut c_void,
) {
if let Some(ctx_arc) = SHADE_RECOLOR_CONTEXTS.get(recolor_ctx) {
let mut c = ctx_arc.lock().unwrap();
c.opaque = opaque;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_recolor_shade(
ctx: ContextHandle,
shade: ShadeHandle,
recolor_ctx: Handle,
) -> ShadeHandle {
let ctx_arc = match SHADE_RECOLOR_CONTEXTS.get(recolor_ctx) {
Some(a) => a,
None => return 0,
};
let (src_cs, dst_cs, vertex_fn, opaque) = match ctx_arc.lock() {
Ok(c) => (c.src_colorspace, c.dst_colorspace, c.vertex_fn, c.opaque),
Err(_) => return 0,
};
if crate::ffi::shade::SHADES.get(shade).is_none() {
return 0;
}
if let Some(vfn) = vertex_fn {
let mut src_sample = [0.0f32; 4];
let mut dst_sample = [0.0f32; 4];
vfn(
ctx,
opaque,
dst_cs,
dst_sample.as_mut_ptr(),
src_cs,
src_sample.as_ptr(),
);
}
shade
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_convert_color(
_ctx: ContextHandle,
_src_cs: ColorspaceHandle,
src: *const f32,
src_n: i32,
_dst_cs: ColorspaceHandle,
dst: *mut f32,
dst_n: i32,
) {
if src.is_null() || dst.is_null() {
return;
}
unsafe {
match (src_n, dst_n) {
(1, 3) => {
let gray = *src;
*dst = gray;
*dst.add(1) = gray;
*dst.add(2) = gray;
}
(3, 1) => {
let r = *src;
let g = *src.add(1);
let b = *src.add(2);
*dst = 0.299 * r + 0.587 * g + 0.114 * b;
}
(4, 3) => {
let c = *src;
let m = *src.add(1);
let y = *src.add(2);
let k = *src.add(3);
*dst = (1.0 - c) * (1.0 - k);
*dst.add(1) = (1.0 - m) * (1.0 - k);
*dst.add(2) = (1.0 - y) * (1.0 - k);
}
(3, 4) => {
let r = *src;
let g = *src.add(1);
let b = *src.add(2);
let k = 1.0 - r.max(g).max(b);
if k < 1.0 {
*dst = (1.0 - r - k) / (1.0 - k);
*dst.add(1) = (1.0 - g - k) / (1.0 - k);
*dst.add(2) = (1.0 - b - k) / (1.0 - k);
} else {
*dst = 0.0;
*dst.add(1) = 0.0;
*dst.add(2) = 0.0;
}
*dst.add(3) = k;
}
_ if src_n == dst_n => {
for i in 0..src_n as usize {
*dst.add(i) = *src.add(i);
}
}
_ => {
for i in 0..dst_n as usize {
*dst.add(i) = 0.0;
}
}
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_gray_to_rgb(gray: f32, r: *mut f32, g: *mut f32, b: *mut f32) {
if !r.is_null() {
unsafe { *r = gray };
}
if !g.is_null() {
unsafe { *g = gray };
}
if !b.is_null() {
unsafe { *b = gray };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_rgb_to_gray(r: f32, g: f32, b: f32) -> f32 {
0.299 * r + 0.587 * g + 0.114 * b
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_cmyk_to_rgb(
c: f32,
m: f32,
y: f32,
k: f32,
r: *mut f32,
g: *mut f32,
b: *mut f32,
) {
if !r.is_null() {
unsafe { *r = (1.0 - c) * (1.0 - k) };
}
if !g.is_null() {
unsafe { *g = (1.0 - m) * (1.0 - k) };
}
if !b.is_null() {
unsafe { *b = (1.0 - y) * (1.0 - k) };
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_rgb_to_cmyk(
r: f32,
g: f32,
b: f32,
c: *mut f32,
m: *mut f32,
y: *mut f32,
k: *mut f32,
) {
let k_val = 1.0 - r.max(g).max(b);
if !k.is_null() {
unsafe { *k = k_val };
}
if k_val < 1.0 {
if !c.is_null() {
unsafe { *c = (1.0 - r - k_val) / (1.0 - k_val) };
}
if !m.is_null() {
unsafe { *m = (1.0 - g - k_val) / (1.0 - k_val) };
}
if !y.is_null() {
unsafe { *y = (1.0 - b - k_val) / (1.0 - k_val) };
}
} else {
if !c.is_null() {
unsafe { *c = 0.0 };
}
if !m.is_null() {
unsafe { *m = 0.0 };
}
if !y.is_null() {
unsafe { *y = 0.0 };
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_convert_to_grayscale(
ctx: ContextHandle,
doc: DocumentHandle,
) -> RecolorStats {
let opts = RecolorOptions::gray();
pdf_recolor_document(ctx, doc, &opts)
}
fn find_page_dict_range(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_in(before, b" obj") {
let obj_abs = search_start + obj_rel;
let after = &data[obj_abs..];
if let Some(dict_rel) = find_in(after, b"<<") {
let dict_start = obj_abs + dict_rel;
if let Some(dict_end) = find_dict_end_pos(data, dict_start) {
return Some((dict_start, dict_end + 2));
}
}
}
}
found += 1;
}
}
i += 1;
}
None
}
fn find_in(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
(0..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle)
}
fn rfind_in(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
(0..=haystack.len() - needle.len())
.rev()
.find(|&i| &haystack[i..i + needle.len()] == needle)
}
fn find_dict_end_pos(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 extract_contents_stream(
data: &[u8],
dict_start: usize,
dict_end: usize,
) -> Option<(Vec<u8>, usize, usize)> {
let end = dict_end.min(data.len());
let region = &data[dict_start..end];
let contents_key = b"/Contents";
let ck_pos = find_in(region, contents_key)?;
let abs_pos = dict_start + ck_pos + contents_key.len();
let mut i = abs_pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i].is_ascii_digit() {
let obj_num = parse_int_at(data, i)?;
let stream_data = extract_stream_for_obj(data, obj_num)?;
let (stream_start, stream_end) = find_stream_boundaries(data, obj_num)?;
Some((stream_data, stream_start, stream_end))
} else if i < data.len() && data[i] == b'[' {
i += 1;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
if i < data.len() && data[i].is_ascii_digit() {
let obj_num = parse_int_at(data, i)?;
let stream_data = extract_stream_for_obj(data, obj_num)?;
let (stream_start, stream_end) = find_stream_boundaries(data, obj_num)?;
Some((stream_data, stream_start, stream_end))
} else {
None
}
} else {
None
}
}
fn parse_int_at(data: &[u8], pos: usize) -> Option<i32> {
let mut i = pos;
while i < data.len() && data[i].is_ascii_whitespace() {
i += 1;
}
let start = i;
while i < data.len() && data[i].is_ascii_digit() {
i += 1;
}
if i > start {
std::str::from_utf8(&data[start..i])
.ok()
.and_then(|s| s.parse().ok())
} else {
None
}
}
fn extract_stream_for_obj(data: &[u8], obj_num: i32) -> Option<Vec<u8>> {
let pattern = format!("{} 0 obj", obj_num);
let obj_pos = rfind_in(data, pattern.as_bytes())?;
let after = &data[obj_pos..];
let stream_kw = find_in(after, b"stream")?;
let mut stream_start = obj_pos + stream_kw + 6;
if stream_start < data.len() && data[stream_start] == b'\r' {
stream_start += 1;
}
if stream_start < data.len() && data[stream_start] == b'\n' {
stream_start += 1;
}
let endstream_pos = find_in(&data[stream_start..], b"endstream")?;
let mut stream_end = stream_start + endstream_pos;
while stream_end > stream_start && data[stream_end - 1].is_ascii_whitespace() {
stream_end -= 1;
}
Some(data[stream_start..stream_end].to_vec())
}
fn find_stream_boundaries(data: &[u8], obj_num: i32) -> Option<(usize, usize)> {
let pattern = format!("{} 0 obj", obj_num);
let obj_pos = rfind_in(data, pattern.as_bytes())?;
let after = &data[obj_pos..];
let stream_kw = find_in(after, b"stream")?;
let mut stream_start = obj_pos + stream_kw + 6;
if stream_start < data.len() && data[stream_start] == b'\r' {
stream_start += 1;
}
if stream_start < data.len() && data[stream_start] == b'\n' {
stream_start += 1;
}
let endstream_pos = find_in(&data[stream_start..], b"endstream")?;
let mut stream_end = stream_start + endstream_pos;
while stream_end > stream_start && data[stream_end - 1].is_ascii_whitespace() {
stream_end -= 1;
}
Some((stream_start, stream_end))
}
fn recolor_page_in_data(data: &[u8], page_num: i32, target_components: i32) -> Option<Vec<u8>> {
let (dict_start, dict_end) = find_page_dict_range(data, page_num)?;
let (stream_data, stream_start, stream_end) =
extract_contents_stream(data, dict_start, dict_end)?;
let recolored_stream = recolor_content_stream(&stream_data, target_components);
if recolored_stream == stream_data {
return None;
}
let new_len = recolored_stream.len();
let mut new_data = Vec::with_capacity(data.len() + recolored_stream.len() - stream_data.len());
new_data.extend_from_slice(&data[..stream_start]);
new_data.extend_from_slice(&recolored_stream);
let dict_region_end = stream_start; let length_key = b"/Length";
if let Some(lk_pos) = find_in(&new_data[..dict_region_end], length_key) {
let after_key = lk_pos + length_key.len();
let mut vs = after_key;
while vs < dict_region_end && new_data[vs].is_ascii_whitespace() {
vs += 1;
}
let mut ve = vs;
while ve < dict_region_end && new_data[ve].is_ascii_digit() {
ve += 1;
}
if ve > vs {
let new_len_str = format!("{}", new_len);
let old_len = ve - vs;
if new_len_str.len() != old_len {
let mut adjusted = Vec::with_capacity(new_data.len());
adjusted.extend_from_slice(&new_data[..vs]);
adjusted.extend_from_slice(new_len_str.as_bytes());
adjusted.extend_from_slice(&new_data[ve..]);
adjusted.extend_from_slice(&data[stream_end..]);
return Some(adjusted);
} else {
new_data[vs..ve].copy_from_slice(new_len_str.as_bytes());
}
}
}
new_data.extend_from_slice(&data[stream_end..]);
Some(new_data)
}
fn recolor_content_stream(stream: &[u8], target_components: i32) -> Vec<u8> {
let text = match std::str::from_utf8(stream) {
Ok(t) => t,
Err(_) => return stream.to_vec(),
};
let mut output = String::with_capacity(text.len());
let mut operand_stack: Vec<String> = Vec::new();
for token in ContentTokenizer::new(text) {
match token {
ContentToken::Number(s) => {
operand_stack.push(s);
}
ContentToken::Operator(op) => {
match op.as_str() {
"g" | "G" => {
let converted = convert_operands(&operand_stack, 1, target_components);
output.push_str(&converted.operands);
output.push_str(&converted.operator);
output.push('\n');
operand_stack.clear();
}
"rg" | "RG" => {
let converted = convert_operands(&operand_stack, 3, target_components);
output.push_str(&converted.operands);
output.push_str(&converted.operator);
output.push('\n');
operand_stack.clear();
}
"k" | "K" => {
let converted = convert_operands(&operand_stack, 4, target_components);
output.push_str(&converted.operands);
output.push_str(&converted.operator);
output.push('\n');
operand_stack.clear();
}
"cs" => {
let cs_name = target_colorspace_name(target_components);
output.push_str(&format!("{} cs\n", cs_name));
operand_stack.clear();
}
"CS" => {
let cs_name = target_colorspace_name(target_components);
output.push_str(&format!("{} CS\n", cs_name));
operand_stack.clear();
}
"sc" | "SC" | "scn" | "SCN" => {
let n = operand_stack.len() as i32;
if n > 0 && (n == 1 || n == 3 || n == 4) {
let converted = convert_operands(&operand_stack, n, target_components);
output.push_str(&converted.operands);
output.push_str(&op);
output.push('\n');
} else {
for s in &operand_stack {
output.push_str(s);
output.push(' ');
}
output.push_str(&op);
output.push('\n');
}
operand_stack.clear();
}
_ => {
for s in &operand_stack {
output.push_str(s);
output.push(' ');
}
output.push_str(&op);
output.push('\n');
operand_stack.clear();
}
}
}
ContentToken::Other(s) => {
operand_stack.push(s);
}
}
}
for s in &operand_stack {
output.push_str(s);
output.push(' ');
}
output.into_bytes()
}
struct ConvertedColor {
operands: String,
operator: String,
}
fn convert_operands(
operands: &[String],
src_components: i32,
dst_components: i32,
) -> ConvertedColor {
let src_values: Vec<f32> = operands
.iter()
.rev()
.take(src_components as usize)
.rev()
.filter_map(|s| s.parse::<f32>().ok())
.collect();
if src_values.len() != src_components as usize {
let mut ops = String::new();
for s in operands {
ops.push_str(s);
ops.push(' ');
}
let op_name = match dst_components {
1 => "g",
3 => "rg",
4 => "k",
_ => "rg",
};
return ConvertedColor {
operands: ops,
operator: op_name.to_string(),
};
}
let (dst_values, op_name) = match (src_components, dst_components) {
(n, m) if n == m => (
src_values.clone(),
match n {
1 => "g",
3 => "rg",
4 => "k",
_ => "rg",
},
),
(1, 3) => {
let g = src_values[0];
(vec![g, g, g], "rg")
}
(1, 4) => {
let g = src_values[0];
(vec![0.0, 0.0, 0.0, 1.0 - g], "k")
}
(3, 1) => {
let gray = 0.299 * src_values[0] + 0.587 * src_values[1] + 0.114 * src_values[2];
(vec![gray], "g")
}
(3, 4) => {
let (r, g, b) = (src_values[0], src_values[1], src_values[2]);
let k = 1.0 - r.max(g).max(b);
let (c, m, y) = if k < 1.0 {
(
(1.0 - r - k) / (1.0 - k),
(1.0 - g - k) / (1.0 - k),
(1.0 - b - k) / (1.0 - k),
)
} else {
(0.0, 0.0, 0.0)
};
(vec![c, m, y, k], "k")
}
(4, 1) => {
let (c, m, y, k) = (src_values[0], src_values[1], src_values[2], src_values[3]);
let r = (1.0 - c) * (1.0 - k);
let g = (1.0 - m) * (1.0 - k);
let b = (1.0 - y) * (1.0 - k);
let gray = 0.299 * r + 0.587 * g + 0.114 * b;
(vec![gray], "g")
}
(4, 3) => {
let (c, m, y, k) = (src_values[0], src_values[1], src_values[2], src_values[3]);
let r = (1.0 - c) * (1.0 - k);
let g = (1.0 - m) * (1.0 - k);
let b = (1.0 - y) * (1.0 - k);
(vec![r, g, b], "rg")
}
_ => {
let vals = vec![0.0f32; dst_components as usize];
let op = match dst_components {
1 => "g",
3 => "rg",
4 => "k",
_ => "rg",
};
(vals, op)
}
};
let mut ops = String::new();
for v in &dst_values {
let formatted = format!("{:.4}", v);
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
ops.push_str(trimmed);
ops.push(' ');
}
ConvertedColor {
operands: ops,
operator: op_name.to_string(),
}
}
fn target_colorspace_name(components: i32) -> &'static str {
match components {
1 => "/DeviceGray",
3 => "/DeviceRGB",
4 => "/DeviceCMYK",
_ => "/DeviceRGB",
}
}
enum ContentToken {
Number(String),
Operator(String),
Other(String),
}
struct ContentTokenizer<'a> {
input: &'a str,
pos: usize,
}
impl<'a> ContentTokenizer<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
fn skip_whitespace(&mut self) {
let bytes = self.input.as_bytes();
while self.pos < bytes.len() {
match bytes[self.pos] {
b' ' | b'\t' | b'\r' | b'\n' | b'\x0c' | b'\x00' => {
self.pos += 1;
}
b'%' => {
while self.pos < bytes.len()
&& bytes[self.pos] != b'\n'
&& bytes[self.pos] != b'\r'
{
self.pos += 1;
}
}
_ => break,
}
}
}
}
impl Iterator for ContentTokenizer<'_> {
type Item = ContentToken;
fn next(&mut self) -> Option<ContentToken> {
self.skip_whitespace();
let bytes = self.input.as_bytes();
if self.pos >= bytes.len() {
return None;
}
let ch = bytes[self.pos];
if ch == b'(' {
let start = self.pos;
self.pos += 1;
let mut depth = 1i32;
while self.pos < bytes.len() && depth > 0 {
match bytes[self.pos] {
b'(' => depth += 1,
b')' => depth -= 1,
b'\\' => {
self.pos += 1; }
_ => {}
}
if depth > 0 {
self.pos += 1;
}
}
if self.pos < bytes.len() {
self.pos += 1; }
let token = &self.input[start..self.pos];
return Some(ContentToken::Other(token.to_string()));
}
if ch == b'<' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] != b'<' {
let start = self.pos;
self.pos += 1;
while self.pos < bytes.len() && bytes[self.pos] != b'>' {
self.pos += 1;
}
if self.pos < bytes.len() {
self.pos += 1;
}
let token = &self.input[start..self.pos];
return Some(ContentToken::Other(token.to_string()));
}
if ch == b'<' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'<' {
self.pos += 2;
return Some(ContentToken::Other("<<".to_string()));
}
if ch == b'>' && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'>' {
self.pos += 2;
return Some(ContentToken::Other(">>".to_string()));
}
if ch == b'[' || ch == b']' {
let token = &self.input[self.pos..self.pos + 1];
self.pos += 1;
return Some(ContentToken::Other(token.to_string()));
}
if ch == b'/' {
let start = self.pos;
self.pos += 1;
while self.pos < bytes.len()
&& !bytes[self.pos].is_ascii_whitespace()
&& bytes[self.pos] != b'/'
&& bytes[self.pos] != b'('
&& bytes[self.pos] != b')'
&& bytes[self.pos] != b'<'
&& bytes[self.pos] != b'>'
&& bytes[self.pos] != b'['
&& bytes[self.pos] != b']'
{
self.pos += 1;
}
let token = &self.input[start..self.pos];
return Some(ContentToken::Other(token.to_string()));
}
if ch.is_ascii_digit() || ch == b'-' || ch == b'+' || ch == b'.' {
let start = self.pos;
if ch == b'-' || ch == b'+' {
self.pos += 1;
}
let mut has_digit = false;
while self.pos < bytes.len() && bytes[self.pos].is_ascii_digit() {
self.pos += 1;
has_digit = true;
}
if self.pos < bytes.len() && bytes[self.pos] == b'.' {
self.pos += 1;
while self.pos < bytes.len() && bytes[self.pos].is_ascii_digit() {
self.pos += 1;
has_digit = true;
}
}
if has_digit {
let token = &self.input[start..self.pos];
return Some(ContentToken::Number(token.to_string()));
}
self.pos = start;
}
if ch.is_ascii_alphabetic() || ch == b'\'' || ch == b'"' {
let start = self.pos;
self.pos += 1;
while self.pos < bytes.len()
&& (bytes[self.pos].is_ascii_alphabetic()
|| bytes[self.pos] == b'*'
|| bytes[self.pos] == b'\''
|| bytes[self.pos] == b'"')
{
self.pos += 1;
}
let token = &self.input[start..self.pos];
return Some(ContentToken::Operator(token.to_string()));
}
let token = &self.input[self.pos..self.pos + 1];
self.pos += 1;
Some(ContentToken::Other(token.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recolor_constants() {
assert_eq!(RECOLOR_GRAY, 1);
assert_eq!(RECOLOR_RGB, 3);
assert_eq!(RECOLOR_CMYK, 4);
}
#[test]
fn test_recolor_options_gray() {
let opts = RecolorOptions::gray();
assert_eq!(opts.num_comp, 1);
assert!(opts.is_valid());
}
#[test]
fn test_recolor_options_rgb() {
let opts = RecolorOptions::rgb();
assert_eq!(opts.num_comp, 3);
assert!(opts.is_valid());
}
#[test]
fn test_recolor_options_cmyk() {
let opts = RecolorOptions::cmyk();
assert_eq!(opts.num_comp, 4);
assert!(opts.is_valid());
}
#[test]
fn test_recolor_options_invalid() {
let opts = RecolorOptions { num_comp: 5 };
assert!(!opts.is_valid());
let opts = RecolorOptions { num_comp: 0 };
assert!(!opts.is_valid());
}
#[test]
fn test_recolor_vertex() {
let v = RecolorVertex::new(3, 4);
assert_eq!(v.src_color.len(), 3);
assert_eq!(v.dst_color.len(), 4);
}
#[test]
fn test_shade_recolor_context() {
let ctx = ShadeRecolorContext::new();
assert_eq!(ctx.src_colorspace, 0);
assert_eq!(ctx.dst_colorspace, 0);
assert!(ctx.opaque.is_null());
let ctx2 = ShadeRecolorContext::with_colorspaces(1, 2);
assert_eq!(ctx2.src_colorspace, 1);
assert_eq!(ctx2.dst_colorspace, 2);
}
#[test]
fn test_recolor_stats() {
let stats = RecolorStats::default();
assert_eq!(stats.pages_processed, 0);
assert_eq!(stats.colors_converted, 0);
assert_eq!(stats.shades_recolored, 0);
}
#[test]
fn test_ffi_options() {
let gray = pdf_recolor_options_gray();
assert_eq!(gray.num_comp, 1);
let rgb = pdf_recolor_options_rgb();
assert_eq!(rgb.num_comp, 3);
let cmyk = pdf_recolor_options_cmyk();
assert_eq!(cmyk.num_comp, 4);
let custom = pdf_recolor_options_new(3);
assert_eq!(custom.num_comp, 3);
}
#[test]
fn test_ffi_options_valid() {
let valid = pdf_recolor_options_rgb();
assert_eq!(pdf_recolor_options_is_valid(&valid), 1);
let invalid = RecolorOptions { num_comp: 7 };
assert_eq!(pdf_recolor_options_is_valid(&invalid), 0);
}
#[test]
fn test_ffi_shade_context() {
let ctx = 0;
let handle = pdf_new_shade_recolor_context(ctx, 1, 2);
assert!(handle > 0);
pdf_drop_shade_recolor_context(ctx, handle);
}
#[test]
fn test_gray_to_rgb() {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
pdf_gray_to_rgb(0.5, &mut r, &mut g, &mut b);
assert!((r - 0.5).abs() < 0.001);
assert!((g - 0.5).abs() < 0.001);
assert!((b - 0.5).abs() < 0.001);
}
#[test]
fn test_rgb_to_gray() {
let gray = pdf_rgb_to_gray(1.0, 1.0, 1.0);
assert!((gray - 1.0).abs() < 0.001);
let gray = pdf_rgb_to_gray(0.0, 0.0, 0.0);
assert!((gray - 0.0).abs() < 0.001);
let gray = pdf_rgb_to_gray(0.5, 0.5, 0.5);
assert!((gray - 0.5).abs() < 0.001);
}
#[test]
fn test_cmyk_to_rgb() {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
pdf_cmyk_to_rgb(0.0, 0.0, 0.0, 1.0, &mut r, &mut g, &mut b);
assert!((r - 0.0).abs() < 0.001);
assert!((g - 0.0).abs() < 0.001);
assert!((b - 0.0).abs() < 0.001);
pdf_cmyk_to_rgb(0.0, 0.0, 0.0, 0.0, &mut r, &mut g, &mut b);
assert!((r - 1.0).abs() < 0.001);
assert!((g - 1.0).abs() < 0.001);
assert!((b - 1.0).abs() < 0.001);
}
#[test]
fn test_rgb_to_cmyk() {
let mut c = 0.0f32;
let mut m = 0.0f32;
let mut y = 0.0f32;
let mut k = 0.0f32;
pdf_rgb_to_cmyk(0.0, 0.0, 0.0, &mut c, &mut m, &mut y, &mut k);
assert!((k - 1.0).abs() < 0.001);
pdf_rgb_to_cmyk(1.0, 1.0, 1.0, &mut c, &mut m, &mut y, &mut k);
assert!((k - 0.0).abs() < 0.001);
assert!((c - 0.0).abs() < 0.001);
assert!((m - 0.0).abs() < 0.001);
assert!((y - 0.0).abs() < 0.001);
}
#[test]
fn test_convert_color_gray_to_rgb() {
let src = [0.5f32];
let mut dst = [0.0f32; 3];
pdf_convert_color(0, 0, src.as_ptr(), 1, 0, dst.as_mut_ptr(), 3);
assert!((dst[0] - 0.5).abs() < 0.001);
assert!((dst[1] - 0.5).abs() < 0.001);
assert!((dst[2] - 0.5).abs() < 0.001);
}
#[test]
fn test_convert_color_rgb_to_gray() {
let src = [0.5f32, 0.5, 0.5];
let mut dst = [0.0f32];
pdf_convert_color(0, 0, src.as_ptr(), 3, 0, dst.as_mut_ptr(), 1);
assert!((dst[0] - 0.5).abs() < 0.001);
}
#[test]
fn test_convert_color_same() {
let src = [0.1f32, 0.2, 0.3];
let mut dst = [0.0f32; 3];
pdf_convert_color(0, 0, src.as_ptr(), 3, 0, dst.as_mut_ptr(), 3);
assert!((dst[0] - 0.1).abs() < 0.001);
assert!((dst[1] - 0.2).abs() < 0.001);
assert!((dst[2] - 0.3).abs() < 0.001);
}
#[test]
fn test_convert_color_cmyk_to_rgb() {
let src = [0.0f32, 0.0, 0.0, 0.5];
let mut dst = [0.0f32; 3];
pdf_convert_color(0, 0, src.as_ptr(), 4, 0, dst.as_mut_ptr(), 3);
assert!((dst[0] - 0.5).abs() < 0.001);
assert!((dst[1] - 0.5).abs() < 0.001);
assert!((dst[2] - 0.5).abs() < 0.001);
}
#[test]
fn test_convert_color_rgb_to_cmyk() {
let src = [0.5f32, 0.5, 0.5];
let mut dst = [0.0f32; 4];
pdf_convert_color(0, 0, src.as_ptr(), 3, 0, dst.as_mut_ptr(), 4);
assert!(dst[3] < 1.0);
}
#[test]
fn test_convert_color_unsupported() {
let src = [0.5f32, 0.5];
let mut dst = [0.0f32; 4];
pdf_convert_color(0, 0, src.as_ptr(), 2, 0, dst.as_mut_ptr(), 4);
assert_eq!(dst[0], 0.0);
assert_eq!(dst[1], 0.0);
assert_eq!(dst[2], 0.0);
assert_eq!(dst[3], 0.0);
}
#[test]
fn test_convert_color_null() {
let src = [0.5f32];
let mut dst = [0.0f32; 3];
pdf_convert_color(0, 0, std::ptr::null(), 1, 0, dst.as_mut_ptr(), 3);
pdf_convert_color(0, 0, src.as_ptr(), 1, 0, std::ptr::null_mut(), 3);
}
#[test]
fn test_pdf_recolor_options_is_valid_null() {
assert_eq!(pdf_recolor_options_is_valid(std::ptr::null()), 0);
}
#[test]
fn test_pdf_shade_recolor_set_opaque() {
let ctx = 0;
let handle = pdf_new_shade_recolor_context(ctx, 1, 2);
let mut opaque: i32 = 42;
pdf_shade_recolor_set_opaque(
ctx,
handle,
&mut opaque as *mut i32 as *mut std::ffi::c_void,
);
pdf_drop_shade_recolor_context(ctx, handle);
}
#[test]
fn test_pdf_shade_recolor_set_opaque_invalid() {
pdf_shade_recolor_set_opaque(0, 99999, std::ptr::null_mut());
}
#[test]
fn test_pdf_gray_to_rgb_null() {
pdf_gray_to_rgb(
0.5,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
#[test]
fn test_pdf_cmyk_to_rgb_null() {
pdf_cmyk_to_rgb(
0.0,
0.0,
0.0,
0.0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
#[test]
fn test_pdf_rgb_to_cmyk_null() {
pdf_rgb_to_cmyk(
0.5,
0.5,
0.5,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
#[test]
fn test_pdf_rgb_to_cmyk_black() {
let mut c = 1.0f32;
let mut m = 1.0f32;
let mut y = 1.0f32;
let mut k = 1.0f32;
pdf_rgb_to_cmyk(0.0, 0.0, 0.0, &mut c, &mut m, &mut y, &mut k);
assert!((k - 1.0).abs() < 0.001);
assert!((c - 0.0).abs() < 0.001);
assert!((m - 0.0).abs() < 0.001);
assert!((y - 0.0).abs() < 0.001);
}
#[test]
fn test_pdf_convert_to_grayscale() {
let pdf_data = 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 /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 44 >>\nstream\n0.5 g\n100 100 50 50 re f\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f\n0000000009 00000 n\n0000000058 00000 n\n0000000115 00000 n\n0000000172 00000 n\ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n250\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
let stats = pdf_convert_to_grayscale(0, doc);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_document_null_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let stats = pdf_recolor_document(0, doc, std::ptr::null());
assert_eq!(stats.pages_processed, 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_document_invalid_doc() {
let opts = RecolorOptions::gray();
let stats = pdf_recolor_document(0, 99999, &opts);
assert_eq!(stats.pages_processed, 0);
}
#[test]
fn test_pdf_recolor_page_null_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
pdf_recolor_page(0, doc, 0, std::ptr::null());
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_pages_null_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let stats = pdf_recolor_pages(0, doc, 0, 0, std::ptr::null());
assert_eq!(stats.pages_processed, 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_pages_start_gt_end() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let opts = RecolorOptions::gray();
let stats = pdf_recolor_pages(0, doc, 5, 2, &opts);
assert_eq!(stats.pages_processed, 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_recolor_vertex_new() {
let v = RecolorVertex::new(3, 4);
assert_eq!(v.src_color.len(), 3);
assert_eq!(v.dst_color.len(), 4);
}
#[test]
fn test_recolor_options_default() {
let opts = RecolorOptions::default();
assert_eq!(opts.num_comp, 3);
}
#[test]
fn test_pdf_recolor_page_invalid_doc() {
let opts = RecolorOptions::gray();
pdf_recolor_page(0, 99999, 0, &opts);
}
#[test]
fn test_pdf_recolor_page_invalid_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let invalid_opts = RecolorOptions { num_comp: 99 };
pdf_recolor_page(0, doc, 0, &invalid_opts);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_document_valid() {
let pdf_data = 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 /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 20 >>\nstream\n0.5 g\nf\nendstream\nendobj\ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n200\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
let opts = RecolorOptions::gray();
let stats = pdf_recolor_document(0, doc, &opts);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_pages_valid() {
let pdf_data = 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 /Contents 4 0 R >>\nendobj\n4 0 obj\n<< /Length 20 >>\nstream\n0.5 g\nf\nendstream\nendobj\ntrailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n200\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
let opts = RecolorOptions::rgb();
let stats = pdf_recolor_pages(0, doc, 0, 0, &opts);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_pages_invalid_doc() {
let opts = RecolorOptions::gray();
let stats = pdf_recolor_pages(0, 99999, 0, 5, &opts);
assert_eq!(stats.pages_processed, 0);
}
#[test]
fn test_pdf_remove_output_intents() {
let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OutputIntents [<< /Type /OutputIntent >>] >>\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\n150\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
pdf_remove_output_intents(0, doc);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_remove_output_intents_indirect_ref() {
let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OutputIntents 5 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\n5 0 obj\n[<< /Type /OutputIntent >>]\nendobj\ntrailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n200\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
pdf_remove_output_intents(0, doc);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_count_output_intents() {
let pdf_data = b"%PDF-1.4\n1 0 obj\n<< /Type /Catalog /Pages 2 0 R /OutputIntents [<< /Type /OutputIntent >> << /Type /OutputIntent >>] >>\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\n150\n%%EOF";
let doc =
crate::ffi::DOCUMENTS.insert(crate::ffi::document::Document::new(pdf_data.to_vec()));
let count = pdf_count_output_intents(0, doc);
assert!(count >= 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_count_output_intents_no_key() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
assert_eq!(pdf_count_output_intents(0, doc), 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_shade_invalid_recolor_ctx() {
let shade = crate::ffi::shade::SHADES.insert(crate::ffi::shade::Shade {
shade_type: crate::ffi::shade::ShadeType::Function,
..Default::default()
});
let result = pdf_recolor_shade(0, shade, 99999);
assert_eq!(result, 0);
crate::ffi::shade::SHADES.remove(shade);
}
#[test]
fn test_pdf_recolor_shade_invalid_shade() {
let ctx = pdf_new_shade_recolor_context(0, 1, 2);
let result = pdf_recolor_shade(0, 99999, ctx);
assert_eq!(result, 0);
pdf_drop_shade_recolor_context(0, ctx);
}
#[test]
fn test_pdf_recolor_shade_valid() {
let shade = crate::ffi::shade::SHADES.insert(crate::ffi::shade::Shade {
shade_type: crate::ffi::shade::ShadeType::Function,
..Default::default()
});
let ctx = pdf_new_shade_recolor_context(0, 1, 2);
let result = pdf_recolor_shade(0, shade, ctx);
assert_eq!(result, shade);
pdf_drop_shade_recolor_context(0, ctx);
crate::ffi::shade::SHADES.remove(shade);
}
#[test]
fn test_find_in() {
let data = b"hello world";
assert_eq!(find_in(data, b"world"), Some(6));
assert!(find_in(data, b"xyz").is_none());
}
#[test]
fn test_rfind_in() {
let data = b"foo bar foo";
assert_eq!(rfind_in(data, b"foo"), Some(8));
}
#[test]
fn test_find_dict_end_pos() {
let data = b"<< /Key /Value >>";
assert!(find_dict_end_pos(data, 0).is_some());
}
#[test]
fn test_recolor_content_stream_gray_to_rgb() {
let stream = b"0.5 g\n";
let result = recolor_content_stream(stream, 3);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("rg"));
}
#[test]
fn test_recolor_content_stream_rgb_to_gray() {
let stream = b"0.5 0.5 0.5 rg\n";
let result = recolor_content_stream(stream, 1);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("g"));
}
#[test]
fn test_recolor_content_stream_cs_operator() {
let stream = b"/DeviceRGB cs\n";
let result = recolor_content_stream(stream, 1);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("/DeviceGray"));
}
#[test]
fn test_recolor_content_stream_sc_operator() {
let stream = b"0.5 0.5 0.5 sc\n";
let result = recolor_content_stream(stream, 1);
assert!(!result.is_empty());
let text = String::from_utf8_lossy(&result);
assert!(text.contains("sc"));
}
#[test]
fn test_target_colorspace_name() {
assert_eq!(target_colorspace_name(1), "/DeviceGray");
assert_eq!(target_colorspace_name(3), "/DeviceRGB");
assert_eq!(target_colorspace_name(4), "/DeviceCMYK");
assert_eq!(target_colorspace_name(99), "/DeviceRGB");
}
#[test]
fn test_recolor_content_stream_cmyk_operators() {
let stream = b"0 0 0 1 k\n";
let result = recolor_content_stream(stream, 3);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("rg"));
}
#[test]
fn test_recolor_content_stream_cs_cs_operators() {
let stream = b"/DeviceCMYK CS\n";
let result = recolor_content_stream(stream, 1);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("/DeviceGray"));
}
#[test]
fn test_recolor_content_stream_scn_pattern() {
let stream = b"/Pat1 0.5 scn\n";
let result = recolor_content_stream(stream, 1);
assert!(!result.is_empty());
}
#[test]
fn test_recolor_content_stream_gray_to_cmyk() {
let stream = b"0.5 g\n";
let result = recolor_content_stream(stream, 4);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("k"));
}
#[test]
fn test_recolor_content_stream_cmyk_to_gray() {
let stream = b"0 0 0 0.5 k\n";
let result = recolor_content_stream(stream, 1);
let text = String::from_utf8_lossy(&result);
assert!(text.contains("g"));
}
#[test]
fn test_recolor_content_stream_invalid_utf8() {
let stream = &[0xFF, 0xFE, 0xFD];
let result = recolor_content_stream(stream, 3);
assert_eq!(result, stream);
}
#[test]
fn test_convert_color_null_src() {
let mut dst = [0.0f32; 3];
pdf_convert_color(0, 0, std::ptr::null(), 1, 0, dst.as_mut_ptr(), 3);
}
#[test]
fn test_convert_color_null_dst() {
let src = [0.5f32];
pdf_convert_color(0, 0, src.as_ptr(), 1, 0, std::ptr::null_mut(), 3);
}
#[test]
fn test_convert_color_rgb_to_cmyk_white() {
let src = [1.0f32, 1.0, 1.0];
let mut dst = [1.0f32; 4];
pdf_convert_color(0, 0, src.as_ptr(), 3, 0, dst.as_mut_ptr(), 4);
assert!((dst[3] - 0.0).abs() < 0.001);
}
#[test]
fn test_pdf_gray_to_rgb_partial_null() {
let mut r = 0.0f32;
pdf_gray_to_rgb(0.5, &mut r, std::ptr::null_mut(), std::ptr::null_mut());
assert!((r - 0.5).abs() < 0.001);
}
#[test]
fn test_pdf_remove_output_intents_invalid_doc() {
pdf_remove_output_intents(0, 99999);
}
#[test]
fn test_pdf_count_output_intents_invalid_doc() {
assert_eq!(pdf_count_output_intents(0, 99999), 0);
}
#[test]
fn test_pdf_recolor_document_invalid_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let invalid_opts = RecolorOptions { num_comp: 99 };
let stats = pdf_recolor_document(0, doc, &invalid_opts);
assert_eq!(stats.pages_processed, 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_pages_invalid_opts() {
let doc = crate::ffi::DOCUMENTS
.insert(crate::ffi::document::Document::new(b"%PDF-1.0\n".to_vec()));
let invalid_opts = RecolorOptions { num_comp: 2 };
let stats = pdf_recolor_pages(0, doc, 0, 0, &invalid_opts);
assert_eq!(stats.pages_processed, 0);
crate::ffi::DOCUMENTS.remove(doc);
}
#[test]
fn test_pdf_recolor_shade_with_vertex_callback() {
extern "C" fn vertex_fn(
_ctx: u64,
_opaque: *mut std::ffi::c_void,
_dst_cs: u64,
dst: *mut f32,
_src_cs: u64,
src: *const f32,
) {
if !dst.is_null() && !src.is_null() {
unsafe {
*dst = *src;
*dst.add(1) = *src.add(1);
*dst.add(2) = *src.add(2);
}
}
}
let shade = crate::ffi::shade::SHADES.insert(crate::ffi::shade::Shade {
shade_type: crate::ffi::shade::ShadeType::Function,
..Default::default()
});
let ctx = pdf_new_shade_recolor_context(0, 1, 2);
if let Some(ctx_arc) = SHADE_RECOLOR_CONTEXTS.get(ctx) {
let mut c = ctx_arc.lock().unwrap();
c.vertex_fn = Some(vertex_fn);
}
let result = pdf_recolor_shade(0, shade, ctx);
assert_eq!(result, shade);
pdf_drop_shade_recolor_context(0, ctx);
crate::ffi::shade::SHADES.remove(shade);
}
#[test]
fn test_find_in_empty() {
assert!(find_in(b"", b"x").is_none());
assert!(find_in(b"a", b"ab").is_none());
}
#[test]
fn test_rfind_in_empty() {
assert!(rfind_in(b"", b"x").is_none());
}
#[test]
fn test_find_dict_end_pos_invalid() {
let data = b"not a dict";
assert!(find_dict_end_pos(data, 0).is_none());
}
#[test]
fn test_parse_int_at() {
let data = b" 123 ";
assert_eq!(parse_int_at(data, 0), Some(123));
}
}