use stet_graphics::display_list::{MembershipPolicy, OcgVisibility, VisibilityExpr};
use crate::diagnostics::{ParsePhase, Severity, WarningSink};
use crate::objects::{PdfDict, PdfObj};
use crate::resolver::Resolver;
const MAX_VE_DEPTH: u32 = 64;
pub fn build_ocmd_visibility(
resolver: &Resolver,
ocmd: &PdfDict,
default_visible: bool,
sink: &WarningSink,
) -> OcgVisibility {
if let Some(ve_obj) = ocmd.get(b"VE") {
let resolved = resolver.deref(ve_obj).ok();
let view = resolved.as_ref().unwrap_or(ve_obj);
if let Some(arr) = view.as_array() {
if let Some(expr) = parse_visibility_expression(resolver, arr, 0, sink) {
return OcgVisibility::Expression {
expr,
default_visible,
};
}
} else {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/VE expression on OCMD is not an array; ignored",
);
}
}
let mut ocg_ids = Vec::new();
if let Some(ocgs_obj) = ocmd.get(b"OCGs") {
match ocgs_obj {
PdfObj::Ref(num, _) => ocg_ids.push(*num),
PdfObj::Array(arr) => {
for item in arr {
if let Some((num, _)) = item.as_ref() {
ocg_ids.push(num);
}
}
}
_ => {}
}
}
let policy = match ocmd.get_name(b"P") {
Some(b"AllOn") => MembershipPolicy::AllOn,
Some(b"AllOff") => MembershipPolicy::AllOff,
Some(b"AnyOff") => MembershipPolicy::AnyOff,
_ => MembershipPolicy::AnyOn,
};
OcgVisibility::Membership {
ocg_ids,
policy,
default_visible,
}
}
pub fn parse_visibility_expression(
resolver: &Resolver,
arr: &[PdfObj],
depth: u32,
sink: &WarningSink,
) -> Option<VisibilityExpr> {
if depth > MAX_VE_DEPTH {
sink.record(
ParsePhase::Layers,
None,
Severity::Error,
format!("/VE depth limit {MAX_VE_DEPTH} reached; expression truncated"),
);
return None;
}
let head = arr.first()?;
let head_name = head.as_name()?;
let parse_operand = |obj: &PdfObj| -> Option<VisibilityExpr> {
if let Some((ocg_id, _)) = obj.as_ref() {
return Some(VisibilityExpr::Layer(ocg_id));
}
let resolved = resolver.deref(obj).ok();
let view = resolved.as_ref().unwrap_or(obj);
if let Some(sub_arr) = view.as_array() {
return parse_visibility_expression(resolver, sub_arr, depth + 1, sink);
}
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
"/VE operand is neither an OCG ref nor a nested array; dropped",
);
None
};
match head_name {
b"And" | b"Or" => {
let mut operands = Vec::new();
for item in &arr[1..] {
if let Some(o) = parse_operand(item) {
operands.push(o);
}
}
if operands.is_empty() {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!(
"/VE /{} has no usable operands; dropped",
std::str::from_utf8(head_name).unwrap_or("?")
),
);
return None;
}
if head_name == b"And" {
Some(VisibilityExpr::And(operands))
} else {
Some(VisibilityExpr::Or(operands))
}
}
b"Not" => {
if arr.len() != 2 {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!("/VE /Not expects 1 operand, got {}; dropped", arr.len() - 1),
);
return None;
}
let inner = parse_operand(&arr[1])?;
Some(VisibilityExpr::Not(Box::new(inner)))
}
other => {
sink.record(
ParsePhase::Layers,
None,
Severity::Warning,
format!(
"/VE has unknown leading operator /{}; dropped",
String::from_utf8_lossy(other)
),
);
None
}
}
}