ifc_lite_processing/pdf_vector/
report.rs1use super::extent::{self, Rect};
7use serde::Serialize;
8use sha2::{Digest, Sha256};
9use std::collections::BTreeMap;
10
11pub const FIDELITY_ALGORITHM: &str = "ifclite-pdf-fidelity-v1";
12pub const MAX_LISTED_OMISSIONS: usize = 4096;
14
15#[derive(Debug, Clone, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Omission {
18 pub kind: String,
22 pub operator_ordinal: u32,
23 pub bbox_pdf: Option<Rect>,
25 pub visible: bool,
27}
28#[derive(Debug, Clone, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub struct OmissionSummary {
31 pub kind: String,
32 pub count: u32,
33 pub visible_count: u32,
34 pub bbox_pdf: Option<Rect>,
36}
37#[derive(Debug, Clone, Serialize)]
38#[serde(rename_all = "camelCase")]
39pub struct FidelityReport {
40 pub sha256: String,
42 pub algorithm: &'static str,
43 pub exact: bool,
45 pub raster_only: bool,
47 pub convertible_paths: u32,
48 pub omitted_paints: u32,
52 pub summary: Vec<OmissionSummary>,
53 pub omissions: Vec<Omission>,
54 pub omissions_truncated: bool,
55}
56#[derive(Serialize)]
57#[serde(rename_all = "camelCase")]
58struct Verdict<'a> {
59 exact: bool,
60 raster_only: bool,
61 convertible_paths: u32,
62 omitted_paints: u32,
63 summary: &'a [OmissionSummary],
64}
65
66#[derive(Default)]
67pub(super) struct ReportBuilder {
68 omissions: Vec<Omission>,
69 truncated: bool,
70 summary: BTreeMap<String, OmissionSummary>,
71 omitted_paints: u32,
72 visible_images: u32,
73 visible_vector: u32,
74}
75impl ReportBuilder {
76 pub fn record(&mut self, kind: &str, ordinal: u32, bbox: Option<Rect>, visible: bool, painted: bool) {
79 if painted && visible {
80 self.omitted_paints += 1;
81 }
82 if visible {
83 if kind == "image" {
84 self.visible_images += 1;
85 } else if kind != "hidden" {
86 self.visible_vector += 1;
87 }
88 }
89 let entry = self.summary.entry(kind.to_owned()).or_insert_with(|| OmissionSummary {
90 kind: kind.to_owned(),
91 count: 0,
92 visible_count: 0,
93 bbox_pdf: None,
94 });
95 entry.count += 1;
96 if visible {
97 entry.visible_count += 1;
98 entry.bbox_pdf = extent::union(entry.bbox_pdf, bbox);
99 }
100 if self.omissions.len() < MAX_LISTED_OMISSIONS {
101 self.omissions.push(Omission {
102 kind: kind.to_owned(),
103 operator_ordinal: ordinal,
104 bbox_pdf: bbox,
105 visible,
106 });
107 } else {
108 self.truncated = true;
109 }
110 }
111 pub fn finish(self, request_sha256: &str, convertible_paths: usize) -> FidelityReport {
112 let mut summary: Vec<_> = self.summary.into_values().collect();
113 summary.sort_by(|a, b| {
114 b.visible_count
115 .cmp(&a.visible_count)
116 .then(b.count.cmp(&a.count))
117 .then(a.kind.cmp(&b.kind))
118 });
119 let convertible_paths = u32::try_from(convertible_paths).unwrap_or(u32::MAX);
120 let raster_only = convertible_paths == 0 && self.visible_vector == 0 && self.visible_images > 0;
121 let exact = !raster_only && summary.iter().all(|s| s.visible_count == 0);
122 let verdict = Verdict {
123 exact,
124 raster_only,
125 convertible_paths,
126 omitted_paints: self.omitted_paints,
127 summary: &summary,
128 };
129 let mut hash = Sha256::new();
130 hash.update(FIDELITY_ALGORITHM.as_bytes());
131 hash.update(b"\0");
132 hash.update(request_sha256.as_bytes());
133 hash.update(serde_json::to_vec(&verdict).unwrap_or_default());
134 FidelityReport {
135 sha256: format!("{:x}", hash.finalize()),
136 algorithm: FIDELITY_ALGORITHM,
137 exact,
138 raster_only,
139 convertible_paths,
140 omitted_paints: self.omitted_paints,
141 summary,
142 omissions: self.omissions,
143 omissions_truncated: self.truncated,
144 }
145 }
146}
147impl FidelityReport {
148 pub fn visible_omissions(&self) -> u32 {
149 self.summary.iter().map(|s| s.visible_count).sum()
150 }
151 pub fn describe(&self) -> String {
155 if self.raster_only {
156 return "raster-only page".into();
157 }
158 if self.exact {
159 return "exact conversion".into();
160 }
161 let parts: Vec<String> = self
162 .summary
163 .iter()
164 .filter(|s| s.visible_count > 0)
165 .map(|s| format!("{} {}", s.visible_count, kind_label(&s.kind, s.visible_count)))
166 .collect();
167 format!("partial conversion; omitted {}", parts.join(", "))
168 }
169}
170
171pub fn kind_label(kind: &str, count: u32) -> String {
173 let plural = |one: &str, many: &str| if count == 1 { one } else { many }.to_owned();
174 match kind {
175 "text" => plural("text run", "text runs"),
176 "image" => plural("image", "images"),
177 "clip" => plural("clipped path", "clipped paths"),
178 "transparency" => plural("transparent path", "transparent paths"),
179 "pattern" => plural("pattern paint", "pattern paints"),
180 "dash" => plural("dashed stroke", "dashed strokes"),
181 "roundCapJoin" => plural("round-cap/join stroke", "round-cap/join strokes"),
182 "curvedStroke" => plural("curved stroke", "curved strokes"),
183 "hairline" => plural("hairline stroke", "hairline strokes"),
184 "hidden" => plural("hidden path", "hidden paths"),
185 "annotation" => plural("annotation appearance", "annotation appearances"),
186 other => match other.strip_prefix("unsupported:") {
187 Some(op) => format!("{} under unsupported operator {op}", plural("entry", "entries")),
188 None => other.to_owned(),
189 },
190 }
191}