use super::*;
use crate::appearance::{
tests::{apply, CONTROLLED_IFC},
AnnotationPlaneFrame,
};
use crate::pdf_vector::*;
fn rectangle(x: f64, y: f64, w: f64, h: f64) -> Vec<f64> {
vec![0., x, y, 1., x + w, y, 1., x + w, y + h, 1., x, y + h, 4.]
}
fn fixture() -> (String, PdfFillAnnotationRequest) {
let source=CONTROLLED_IFC.replace("#37=IFCINDEXEDTRIANGLETEXTUREMAP", "#40=IFCBUILDINGSTOREY('0Storey0000000000000000',$,'Level',$,$,#11,$,$,.ELEMENT.,0.);\n#37=IFCINDEXEDTRIANGLETEXTUREMAP");
let mut outer = rectangle(0., 0., 10., 10.);
outer.extend(rectangle(2., 2., 1., 1.));
let ops = vec![
PdfVectorOperator::FillColor { rgb: [1., 0., 0.] },
PdfVectorOperator::Path {
paint: PdfVectorPaint::EvenOddFill,
commands: outer,
},
PdfVectorOperator::FillColor { rgb: [0., 0., 1.] },
PdfVectorOperator::Path {
paint: PdfVectorPaint::Fill,
commands: rectangle(4., 4., 2., 2.),
},
];
(
source,
PdfFillAnnotationRequest {
schema: "IFC4".into(),
source_revision: "pdf-fill-test".into(),
next_express_id: 100,
container_id: 40,
global_id: "0aaaaaaaaaaaaaaaaaaaaa".into(),
containment_global_id: "0bbbbbbbbbbbbbbbbbbbbb".into(),
name: "PDF fill plan".into(),
frame: AnnotationPlaneFrame {
origin: [2., 3., 4.],
axis_u: [1., 0., 0.],
axis_v: [0., 0., 1.],
size_metres: [8., 8.],
},
page: PdfVectorPage {
pdf_sha256: "a".repeat(64),
decoder_version: "6.3.289".into(),
page_number: 1,
view_box: [0., 0., 8., 8.],
user_unit: 1.,
intrinsic_rotation: 0,
model_metres_from_pdf: [1., 0., 0., 1., 0., 0.],
calibration_key: "calibration-v1".into(),
tolerance_metres: 0.0001,
operations: ops
.into_iter()
.enumerate()
.map(|(i, operation)| PdfVectorOperation {
ordinal: i as u32,
operation,
})
.collect(),
},
},
)
}
fn area(mesh: &crate::types::mesh::MeshData) -> f64 {
mesh.indices
.chunks_exact(3)
.map(|t| {
let p: Vec<_> = t
.iter()
.map(|i| &mesh.positions[*i as usize * 3..*i as usize * 3 + 3])
.collect();
let a: [f64; 3] = std::array::from_fn(|i| f64::from(p[1][i] - p[0][i]));
let b: [f64; 3] = std::array::from_fn(|i| f64::from(p[2][i] - p[0][i]));
let c = [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
c.iter().map(|v| v * v).sum::<f64>().sqrt() / 2.
})
.sum()
}
#[test]
fn issue_4459_direct_pdf_fill_provenance_matches_mesh_without_removing_2d_symbols() {
let (source, request) = fixture();
let plan = plan_pdf_fill_annotation(source.as_bytes(), &request).unwrap();
let exported = apply(&source, &plan.plan);
let geometry = crate::process_geometry(exported.as_bytes());
let (symbols, items) = crate::symbolic::extract_symbolic_data_with_provenance(exported.as_bytes()).into_parts();
let fills: Vec<_> = symbols.fills.iter().enumerate().filter(|(_, f)| f.express_id == plan.annotation_id).collect();
assert_eq!(fills.len(), 2, "2D drawing primitives must remain available");
for (ordinal, fill) in fills {
let item = items[ordinal].expect("direct fill must identify its source item");
assert!(geometry.meshes.iter().any(|m| m.express_id == fill.express_id
&& m.geometry_item_id == Some(item) && !m.indices.is_empty()));
}
}
#[test]
fn issue_4406_fill_page_preserves_evenodd_hole_crop_paint_order_and_native_reopen() {
let (source, request) = fixture();
let plan = plan_pdf_fill_annotation(source.as_bytes(), &request).unwrap();
assert_eq!(plan.regions.len(), 2);
assert_eq!(plan.meshes.len(), 2);
let reopened = crate::process_geometry(apply(&source, &plan.plan).as_bytes());
let meshes: Vec<_> = reopened
.meshes
.iter()
.filter(|m| m.express_id == plan.annotation_id)
.collect();
assert_eq!(meshes.len(), 2);
for m in &plan.meshes {
let restored = meshes
.iter()
.find(|r| r.geometry_item_id == m.geometry_item_id)
.unwrap();
assert_eq!(restored.positions, m.positions);
assert_eq!(restored.indices, m.indices);
assert_eq!(restored.color, m.color);
let expected = if m.color == [1., 0., 0., 1.] {
59.
} else {
assert_eq!(m.color, [0., 0., 1., 1.]);
4.
};
assert!(
(area(m) - expected).abs() < 1e-6,
"{} != {expected}",
area(m)
);
for p in m.positions.chunks_exact(3) {
let world: [f64; 3] =
std::array::from_fn(|i| f64::from(p[i]) + m.origin[i] + plan.rtc_offset[i]);
assert!((world[1] - 3.).abs() < 1e-6);
assert!(world[0] >= 2. - 1e-6 && world[0] <= 10. + 1e-6);
assert!(world[2] >= 4. - 1e-6 && world[2] <= 12. + 1e-6);
}
}
}
#[test]
fn issue_4406_fill_page_refuses_all_output_for_curved_stroke_or_unsupported_state() {
let (source, request) = fixture();
for operation in [
PdfVectorOperator::Path {
paint: PdfVectorPaint::Stroke,
commands: vec![0., 0., 0., 2., 0., 1., 1., 1., 1., 0.],
},
PdfVectorOperator::Path {
paint: PdfVectorPaint::Fill,
commands: vec![0., 0., 0., 2., 0., 1., 1., 1., 1., 0., 4.],
},
PdfVectorOperator::Unsupported {
operator: "paintImageXObject".into(),
},
] {
let mut changed = request.clone();
changed.page.operations.push(PdfVectorOperation {
ordinal: 99,
operation,
});
assert!(plan_pdf_fill_annotation(source.as_bytes(), &changed).is_err());
}
}
#[test]
fn issue_4406_fill_page_refuses_subgrid_hole_and_near_touching_island() {
let (source, request) = fixture();
for commands in [
{
let mut p = rectangle(0., 0., 8., 8.);
p.extend(rectangle(2., 2., 1e-9, 1.));
p
},
{
let mut p = rectangle(0., 0., 2., 2.);
p.extend(rectangle(2. + 1e-8, 0.5, 1., 1.));
p
},
] {
let mut changed = request.clone();
changed.page.operations = vec![PdfVectorOperation {
ordinal: 1,
operation: PdfVectorOperator::Path {
paint: PdfVectorPaint::EvenOddFill,
commands,
},
}];
let error = plan_pdf_fill_annotation(source.as_bytes(), &changed).unwrap_err();
assert!(
error.contains("quantization") || error.contains("too close"),
"{error}"
);
}
}
#[test]
fn issue_4406_nonzero_winding_and_millimetre_horizontal_placement_preserve_areas() {
let (source, mut request) = fixture();
let source = source.replace(".LENGTHUNIT.,$,.METRE.", ".LENGTHUNIT.,.MILLI.,.METRE.");
request.frame.axis_v = [0., 1., 0.];
for reversed in [false, true] {
let mut commands = rectangle(0., 0., 10., 10.);
if reversed {
commands.extend([0., 2., 2., 1., 2., 3., 1., 3., 3., 1., 3., 2., 4.]);
} else {
commands.extend(rectangle(2., 2., 1., 1.));
}
request.page.operations[1].operation = PdfVectorOperator::Path {
paint: PdfVectorPaint::Fill,
commands,
};
let plan = plan_pdf_fill_annotation(source.as_bytes(), &request).unwrap();
let red = plan
.meshes
.iter()
.find(|m| m.color == [1., 0., 0., 1.])
.unwrap();
assert!((area(red) - if reversed { 59. } else { 60. }).abs() < 1e-6);
for m in &plan.meshes {
for p in m.positions.chunks_exact(3) {
assert!((f64::from(p[2]) + m.origin[2] + plan.rtc_offset[2] - 4.).abs() < 1e-6);
}
}
let restored = crate::process_geometry(apply(&source, &plan.plan).as_bytes());
assert_eq!(
restored
.meshes
.iter()
.filter(|m| m.express_id == plan.annotation_id)
.count(),
plan.meshes.len()
);
}
}
#[test]
fn issue_4406_fill_plan_binds_source_page_frame_and_allocator_and_refuses_bad_metadata() {
let (source, request) = fixture();
let before = plan_pdf_fill_annotation(source.as_bytes(), &request).unwrap();
for i in 0..4 {
let mut changed = request.clone();
match i {
0 => changed.frame.origin[0] += 1.,
1 => changed.next_express_id += 10,
2 => changed.page.pdf_sha256 = "b".repeat(64),
_ => changed.page.calibration_key = "changed".into(),
}
assert_ne!(
plan_pdf_fill_annotation(source.as_bytes(), &changed)
.unwrap()
.request_sha256,
before.request_sha256
);
}
let mut changed = request.clone();
changed.name = "#123".into();
assert!(plan_pdf_fill_annotation(source.as_bytes(), &changed)
.unwrap_err()
.contains("reserved appearance wire token"));
changed = request.clone();
changed.next_express_id = 40;
assert!(plan_pdf_fill_annotation(source.as_bytes(), &changed)
.unwrap_err()
.contains("allocator"));
}
#[test]
fn issue_4406_convex_bezier_fills_preserve_analytic_area_under_nonuniform_affine() {
let (source, mut request)=fixture();
request.page.tolerance_metres=0.01;
for (commands, exact_area) in [
(vec![0.,2.,2.,2.,2.,5.,5.,5.,5.,2.,4.],5.4),
(vec![0.,2.,2.,3.,2.,5.,5.,5.,1.,5.,2.,4.],7.5),
] {
for affine in [[1.,0.,0.,1.,0.,0.],[2.,0.3,0.5,1.,0.,0.],[-1.,0.,0.,1.,8.,0.]] {
request.page.model_metres_from_pdf=affine;
request.page.operations=vec![PdfVectorOperation{ordinal:0,operation:PdfVectorOperator::Path{
paint:PdfVectorPaint::Fill,commands:commands.clone()}}];
let plan=plan_pdf_fill_annotation(source.as_bytes(),&request).unwrap();
assert_eq!(plan.meshes.len(),1);
let expected=exact_area*(affine[0]*affine[3]-affine[1]*affine[2]).abs();
assert!((area(&plan.meshes[0])-expected).abs()<0.015);
let reopened=crate::process_geometry(apply(&source,&plan.plan).as_bytes());
let mesh=reopened.meshes.iter().find(|m|m.express_id==plan.annotation_id).unwrap();
assert_eq!(mesh.positions,plan.meshes[0].positions);
assert_eq!(mesh.indices,plan.meshes[0].indices);
}
}
}
#[test]
fn issue_4406_curved_fill_refuses_unresolved_concavity_contacts_and_tiny_precision() {
let (source,mut request)=fixture();
request.page.tolerance_metres=0.01;
request.page.operations=vec![PdfVectorOperation{ordinal:0,operation:PdfVectorOperator::Path{
paint:PdfVectorPaint::Fill,commands:vec![0.,2.,2.,2.,2.,5.,5.,5.,5.,2.,4.]}}];
let mut touching=request.clone();
touching.page.operations.push(PdfVectorOperation{ordinal:1,operation:PdfVectorOperator::Path{
paint:PdfVectorPaint::Fill,commands:rectangle(3.,2.,1.,1.)}});
assert!(plan_pdf_fill_annotation(source.as_bytes(),&touching).unwrap_err().contains("error envelope"));
let mut concave=request.clone();
concave.page.operations[0].operation=PdfVectorOperator::Path{paint:PdfVectorPaint::Fill,
commands:vec![0.,2.,2.,2.,2.,5.,5.,5.,5.,2.,1.,3.5,3.,4.]};
assert!(plan_pdf_fill_annotation(source.as_bytes(),&concave).unwrap_err().contains("control hulls"));
request.page.tolerance_metres=1e-9;
assert!(plan_pdf_fill_annotation(source.as_bytes(),&request).is_err());
}
#[test]
fn issue_4406_actual_decoded_ellipse_hole_and_shear_controls_produce_complete_plans() {
let (source, _)=fixture();
for data in [
include_str!("../../../../docs/architecture/evidence/pdf-curved-fill-annotations/page-1-request.json"),
include_str!("../../../../docs/architecture/evidence/pdf-curved-fill-annotations/page-2-request.json"),
include_str!("../../../../docs/architecture/evidence/pdf-curved-fill-annotations/page-3-request.json"),
] {
let request:PdfFillAnnotationRequest=serde_json::from_str(data).unwrap();
let plan=plan_pdf_fill_annotation(source.as_bytes(),&request).unwrap();
assert_eq!(plan.regions.len(),1);
assert_eq!(plan.meshes.len(),1);
assert!(plan.geometry_work<=4_000_000);
assert!(area(&plan.meshes[0])>1.);
}
}
#[path = "pdf_stroke_tests.rs"]
mod stroke_tests;
#[test]
fn issue_4458_registered_cropbox_contacts_survive_classify_clip_and_paint_order() {
let (source,_) = fixture();
let request:PdfFillAnnotationRequest=serde_json::from_str(include_str!("../../../../docs/architecture/evidence/pdf-composition-lattice/registered-request.json")).unwrap();
for tolerance in [0.001,0.0001,0.00001,0.000001,0.0000001] {
let mut request=request.clone();request.page.tolerance_metres=tolerance;
let result=plan_pdf_fill_annotation(source.as_bytes(),&request).unwrap();
assert!((result.meshes.iter().map(area).sum::<f64>()-2.4).abs()<0.00001);
assert_eq!(result.regions.len(),2);
let reopened=crate::process_geometry(apply(&source,&result.plan).as_bytes());
let total:f64=reopened.meshes.iter().filter(|m|m.express_id==result.annotation_id).map(area).sum();
assert!((total-2.4).abs()<0.00001);
}
}