ifc_lite_processing/appearance/
pdf_fill.rs1use super::{
5 authored::{self, refs, vector, Metadata},
6 canonical,
7 pdf_fill_provenance::Provenance,
8 pdf_fill_types::*,
9};
10use ifc_lite_core::{AttributeValue as A, IfcType};
11use rustc_hash::FxHashMap;
12use sha2::{Digest, Sha256};
13
14pub(super) const ALGORITHM: &str = "ifclite-pdf-fill-annotation-v1";
15
16pub fn plan_pdf_fill_annotation(
22 bytes: &[u8],
23 r: &PdfFillAnnotationRequest,
24) -> Result<PdfFillAnnotationPlan, String> {
25 plan_pdf_fill_annotation_with_clip(bytes, r, None)
26}
27
28pub fn plan_pdf_fill_annotation_with_clip(
31 bytes: &[u8],
32 r: &PdfFillAnnotationRequest,
33 conversion_clip_pdf: Option<[f64; 4]>,
34) -> Result<PdfFillAnnotationPlan, String> {
35 let prepared =
36 crate::pdf_vector::prepare_pdf_vector_page_with_clip(&r.page, conversion_clip_pdf)?;
37 let fidelity = &prepared.fidelity;
38 if let Some(accepted) = &r.accepted_fidelity_sha256 {
39 if *accepted != fidelity.sha256 {
40 return Err("Accepted PDF fidelity report does not match this page; review the current report".into());
41 }
42 }
43 let accept_partial = r.accepted_fidelity_sha256.is_some();
44 let geometry = crate::pdf_vector::fills::compose(&prepared, r.page.model_metres_from_pdf, accept_partial)?;
45 let extra_global_ids = [r.property_set_global_id.as_str(), r.property_relation_global_id.as_str()];
46 let metadata = Metadata {
47 schema: &r.schema,
48 source_revision: &r.source_revision,
49 next_express_id: r.next_express_id,
50 container_id: r.container_id,
51 global_id: &r.global_id,
52 containment_global_id: &r.containment_global_id,
53 extra_global_ids: &extra_global_ids,
54 name: &r.name,
55 };
56 let provenance = Provenance {
57 request: r,
58 prepared: &prepared,
59 grid_size_metres: geometry.grid_metres,
60 fill_regions: geometry.shapes.len(),
61 };
62 let mut reserve = 9usize + provenance.reserve();
63 for shape in &geometry.shapes {
64 reserve = reserve
65 .checked_add(4 + shape.rings.len() + shape.rings.iter().map(Vec::len).sum::<usize>())
66 .ok_or("PDF annotation entity budget overflow")?;
67 }
68 let authored::Authoring {
69 mut author,
70 mut source,
71 placement,
72 context_id,
73 owner,
74 scale,
75 rtc_offset,
76 } = authored::prepare(bytes, &metadata, &r.frame, reserve)?;
77 let mut items = Vec::new();
78 let mut styled = Vec::new();
79 let mut regions = Vec::new();
80 for shape in &geometry.shapes {
81 let mut boundaries = Vec::new();
82 for ring in &shape.rings {
83 let mut points = Vec::new();
84 for point in ring {
85 points.push(author.add(
86 IfcType::IfcCartesianPoint,
87 vec![vector([point[0] / scale, point[1] / scale, 0.])],
88 ));
89 }
90 points.push(*points.first().ok_or("PDF fill has an empty boundary")?);
91 boundaries.push(author.add(IfcType::IfcPolyline, vec![refs(&points)]));
92 }
93 let item = author.add(
94 IfcType::IfcAnnotationFillArea,
95 vec![
96 A::EntityRef(boundaries[0]),
97 if boundaries.len() == 1 {
98 A::Null
99 } else {
100 refs(&boundaries[1..])
101 },
102 ],
103 );
104 let rgb = shape.rgb;
105 let color = author.add(
106 IfcType::IfcColourRgb,
107 vec![
108 A::Null,
109 A::Float(rgb[0]),
110 A::Float(rgb[1]),
111 A::Float(rgb[2]),
112 ],
113 );
114 let style = author.add(
115 IfcType::IfcFillAreaStyle,
116 vec![
117 A::String("PDF solid fill".into()),
118 refs(&[color]),
119 A::Enum("T".into()),
120 ],
121 );
122 styled.push((
123 item,
124 author.add(
125 IfcType::IfcStyledItem,
126 vec![A::EntityRef(item), refs(&[style]), A::Null],
127 ),
128 ));
129 items.push(item);
130 regions.push(PdfFillRegion {
131 geometry_item_id: item,
132 source_operator_ordinal: shape.ordinal,
133 rgb,
134 });
135 }
136 let shape = author.add(
137 IfcType::IfcShapeRepresentation,
138 vec![
139 A::EntityRef(context_id),
140 A::String("Annotation".into()),
141 A::String("Annotation2D".into()),
142 refs(&items),
143 ],
144 );
145 let pds = author.add(
146 IfcType::IfcProductDefinitionShape,
147 vec![A::Null, A::Null, refs(&[shape])],
148 );
149 let mut attrs = vec![
150 A::String(r.global_id.clone()),
151 owner.clone(),
152 A::String(r.name.clone()),
153 A::String(format!("PDF vectors, page {}: {}", r.page.page_number, fidelity.describe())),
154 A::String("IfcLite:PdfVectorFills".into()),
155 A::EntityRef(placement),
156 A::EntityRef(pds),
157 ];
158 if r.schema == "IFC4X3" {
159 attrs.push(A::Enum("USERDEFINED".into()));
160 }
161 let annotation = author.add(IfcType::IfcAnnotation, attrs);
162 author.add(
163 IfcType::IfcRelContainedInSpatialStructure,
164 vec![
165 A::String(r.containment_global_id.clone()),
166 owner.clone(),
167 A::Null,
168 A::Null,
169 refs(&[annotation]),
170 A::EntityRef(r.container_id),
171 ],
172 );
173 let property_set_id = provenance.author(&mut author, &owner, annotation)?;
174 if author.plan.created.len() != reserve {
175 return Err("PDF annotation allocation preflight disagrees with authored rows".into());
176 }
177 source.decoder.inject_shared_cache(&author.entities);
178 let mut styles = crate::prepass::ResolvedPrepass::default();
179 for (item, styled) in styled {
180 let info = crate::style::fill::fill_style_from_styled_item(
181 &author.entities[&styled],
182 &mut source.decoder,
183 )
184 .ok_or("Generated PDF fill failed canonical style resolution")?;
185 styles.geometry_style_index.insert(item, info);
186 }
187 let meshes = canonical::produce(
188 &mut source,
189 annotation,
190 &FxHashMap::default(),
191 Some(&styles),
192 )?;
193 if meshes.len() != items.len()
194 || items.iter().any(|item| {
195 meshes
196 .iter()
197 .filter(|m| m.geometry_item_id == Some(*item))
198 .count()
199 != 1
200 })
201 || meshes.iter().any(|m| {
202 m.indices.is_empty() || m.positions.iter().chain(&m.normals).any(|v| !v.is_finite())
203 })
204 {
205 return Err(
206 "Canonical PDF annotation geometry lost or combined a visible fill region".into(),
207 );
208 }
209 if meshes.iter().map(|m| m.positions.len() / 3).sum::<usize>() > 65_536
212 || meshes.iter().map(|m| m.indices.len() / 3).sum::<usize>() > 131_072
213 {
214 return Err("PDF annotation exceeds emitted mesh transport budget".into());
215 }
216 let mut geometry_work = geometry.work;
217 for (shape, region) in geometry.shapes.iter().zip(®ions) {
218 let mesh = meshes
219 .iter()
220 .find(|m| m.geometry_item_id == Some(region.geometry_item_id))
221 .ok_or("Missing PDF fill mesh")?;
222 let expected: Vec<[f64; 3]> = shape
223 .rings
224 .iter()
225 .flatten()
226 .map(|p| {
227 std::array::from_fn(|i| {
228 r.frame.origin[i] + r.frame.axis_u[i] * p[0] + r.frame.axis_v[i] * p[1]
229 })
230 })
231 .collect();
232 let actual: Vec<[f64; 3]> = mesh
233 .positions
234 .chunks_exact(3)
235 .map(|p| std::array::from_fn(|i| f64::from(p[i]) + mesh.origin[i] + rtc_offset[i]))
236 .collect();
237 geometry_work = geometry_work
238 .checked_add((expected.len() as u64) * (actual.len() as u64) * 2)
239 .ok_or("PDF fill precision work overflow")?;
240 if geometry_work > 4_000_000 {
241 return Err("PDF fill canonical precision check exceeds shared work budget".into());
242 }
243 let tolerance2 = r.page.tolerance_metres * r.page.tolerance_metres;
244 let near = |a: &[f64; 3], b: &[f64; 3]| {
245 a.iter().zip(b).map(|(a, b)| (a - b) * (a - b)).sum::<f64>() <= tolerance2
246 };
247 if expected.iter().any(|p| !actual.iter().any(|q| near(p, q)))
248 || actual.iter().any(|p| !expected.iter().any(|q| near(p, q)))
249 {
250 return Err("Canonical PDF fill vertices exceed the requested metric precision; move the page nearer the model origin".into());
251 }
252 }
253 let source_ifc_sha256 = format!("{:x}", Sha256::digest(bytes));
254 let mut digest = Sha256::new();
255 digest.update(ALGORITHM.as_bytes());
256 digest.update(b"\0");
257 digest.update(source_ifc_sha256.as_bytes());
258 digest.update(serde_json::to_vec(r).map_err(|e| e.to_string())?);
259 Ok(PdfFillAnnotationPlan {
260 plan: author.plan,
261 annotation_id: annotation,
262 property_set_id,
263 meshes,
264 coordinate_space: "ifc-z-up",
265 rtc_offset,
266 frame: r.frame.clone(),
267 source_ifc_sha256,
268 source_pdf_sha256: r.page.pdf_sha256.clone(),
269 page_number: r.page.page_number,
270 request_sha256: format!("{:x}", digest.finalize()),
271 algorithm: ALGORITHM,
272 calibration_key: r.page.calibration_key.clone(),
273 tolerance_metres: r.page.tolerance_metres,
274 grid_size_metres: geometry.grid_metres,
275 geometry_work,
276 regions,
277 fidelity: prepared.fidelity,
278 })
279}
280
281#[cfg(test)]
282#[path = "pdf_fill_tests.rs"]
283mod tests;