1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum PdfCompliance {
8 #[default]
10 Standard,
11 PdfA1b,
13 PdfUA1,
15 PdfA1bUA1,
17}
18
19impl PdfCompliance {
20 pub fn requires_pdfa(&self) -> bool {
22 matches!(self, PdfCompliance::PdfA1b | PdfCompliance::PdfA1bUA1)
23 }
24
25 pub fn requires_pdfua(&self) -> bool {
27 matches!(self, PdfCompliance::PdfUA1 | PdfCompliance::PdfA1bUA1)
28 }
29}
30
31pub const SRGB_ICC_PROFILE: &[u8] = &[
49 0x00, 0x00, 0x01, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x02, 0x10, 0x00, 0x00, 0x6D, 0x6E, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x63, 0x73, 0x70, 0x4D, 0x53, 0x46, 0x54, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x43, 0x20, 0x73, 0x52, 0x47, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0xF6, 0xD6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xD3, 0x2D, 0x48, 0x50, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
71 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
73 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
74 0x00, 0x00, 0x00, 0x09, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x64,
80 0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x01, 0x54, 0x00, 0x00, 0x00, 0x2A,
82 0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, 0x7E, 0x00, 0x00, 0x00, 0x14,
84 0x72, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x01, 0x92, 0x00, 0x00, 0x00, 0x14,
86 0x67, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x01, 0xA6, 0x00, 0x00, 0x00, 0x14,
88 0x62, 0x58, 0x59, 0x5A, 0x00, 0x00, 0x01, 0xBA, 0x00, 0x00, 0x00, 0x14,
90 0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x0E,
92 0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x0E,
94 0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x0E,
96 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x73, 0x52, 0x47, 0x42, 0x20, 0x49, 0x45, 0x43, 0x36, 0x31, 0x39, 0x36, 0x36, 0x2D, 0x32, 0x2D, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
105 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
106 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
107 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
108 0x00, 0x00, 0x00, 0x00, 0x00,
109 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6F, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x49, 0x45, 0x43, 0x20, 0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x65, 0x63, 0x2E, 0x63, 0x68, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF6, 0xD6, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xD3, 0x2D, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6E, 0xA2, 0x00, 0x00, 0x38, 0xF2, 0x00, 0x00, 0x03, 0x90, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x99, 0x00, 0x00, 0xB7, 0x85, 0x00, 0x00, 0x18, 0xDA, 0x58, 0x59, 0x5A, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0E, 0x00, 0x00, 0x0B, 0xA3, 0x00, 0x00, 0xB6, 0xCF, 0x63, 0x75, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02,
146 0x33, ];
149
150pub fn generate_xmp_metadata(
155 title: Option<&str>,
156 creator_tool: &str,
157 compliance: PdfCompliance,
158) -> String {
159 let title_str = title.unwrap_or("Untitled");
160
161 let pdfa_part = if compliance.requires_pdfa() {
162 r#" <rdf:Description rdf:about=""
163 xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
164 <pdfaid:part>1</pdfaid:part>
165 <pdfaid:conformance>B</pdfaid:conformance>
166 </rdf:Description>
167"#
168 } else {
169 ""
170 };
171
172 let pdfua_part = if compliance.requires_pdfua() {
173 r#" <rdf:Description rdf:about=""
174 xmlns:pdfuaid="http://www.aiim.org/pdfua/ns/id/">
175 <pdfuaid:part>1</pdfuaid:part>
176 </rdf:Description>
177"#
178 } else {
179 ""
180 };
181
182 format!(
183 "<?xpacket begin=\"\u{FEFF}\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n\
184<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n \
185<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n \
186<rdf:Description rdf:about=\"\"\n \
187xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n \
188xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\">\n \
189<dc:title>\n <rdf:Alt>\n \
190<rdf:li xml:lang=\"x-default\">{title}</rdf:li>\n \
191</rdf:Alt>\n </dc:title>\n \
192<dc:format>application/pdf</dc:format>\n \
193<xmp:CreatorTool>{tool}</xmp:CreatorTool>\n \
194</rdf:Description>\n\
195{pdfa}{pdfua}\
196</rdf:RDF>\n\
197</x:xmpmeta>\n\
198<?xpacket end=\"w\"?>",
199 title = title_str,
200 tool = creator_tool,
201 pdfa = pdfa_part,
202 pdfua = pdfua_part,
203 )
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_compliance_default() {
212 let c = PdfCompliance::default();
213 assert_eq!(c, PdfCompliance::Standard);
214 }
215
216 #[test]
217 fn test_compliance_pdfa_flags() {
218 assert!(PdfCompliance::PdfA1b.requires_pdfa());
219 assert!(!PdfCompliance::PdfA1b.requires_pdfua());
220 assert!(PdfCompliance::PdfA1bUA1.requires_pdfa());
221 assert!(PdfCompliance::PdfA1bUA1.requires_pdfua());
222 assert!(!PdfCompliance::Standard.requires_pdfa());
223 }
224
225 #[test]
226 fn test_xmp_metadata_pdfa() {
227 let xmp = generate_xmp_metadata(Some("Test Doc"), "fop-rs", PdfCompliance::PdfA1b);
228 assert!(xmp.contains("pdfaid:part"));
229 assert!(xmp.contains("<pdfaid:conformance>B</pdfaid:conformance>"));
230 assert!(!xmp.contains("pdfuaid"));
231 }
232
233 #[test]
234 fn test_xmp_metadata_pdfua() {
235 let xmp = generate_xmp_metadata(None, "fop-rs", PdfCompliance::PdfUA1);
236 assert!(!xmp.contains("pdfaid:part"));
237 assert!(xmp.contains("pdfuaid:part"));
238 }
239
240 #[test]
241 fn test_xmp_metadata_combined() {
242 let xmp = generate_xmp_metadata(Some("Test"), "fop-rs", PdfCompliance::PdfA1bUA1);
243 assert!(xmp.contains("pdfaid:part"));
244 assert!(xmp.contains("pdfuaid:part"));
245 }
246
247 #[test]
248 fn test_srgb_icc_profile_size() {
249 assert!(
252 SRGB_ICC_PROFILE.len() >= 128,
253 "ICC profile must be at least 128 bytes (header only)"
254 );
255 let declared = u32::from_be_bytes([
256 SRGB_ICC_PROFILE[0],
257 SRGB_ICC_PROFILE[1],
258 SRGB_ICC_PROFILE[2],
259 SRGB_ICC_PROFILE[3],
260 ]) as usize;
261 assert_eq!(
262 declared,
263 SRGB_ICC_PROFILE.len(),
264 "ICC header declares {declared} bytes but array has {} bytes",
265 SRGB_ICC_PROFILE.len()
266 );
267 }
268}
269
270#[cfg(test)]
271mod tests_extended {
272 use super::*;
273
274 #[test]
275 fn test_compliance_standard_requires_nothing() {
276 let c = PdfCompliance::Standard;
277 assert!(!c.requires_pdfa());
278 assert!(!c.requires_pdfua());
279 }
280
281 #[test]
282 fn test_compliance_pdfua_only() {
283 let c = PdfCompliance::PdfUA1;
284 assert!(!c.requires_pdfa());
285 assert!(c.requires_pdfua());
286 }
287
288 #[test]
289 fn test_compliance_pdfa_variant_name() {
290 assert_ne!(PdfCompliance::Standard, PdfCompliance::PdfA1b);
292 assert_ne!(PdfCompliance::PdfA1b, PdfCompliance::PdfUA1);
293 assert_ne!(PdfCompliance::PdfUA1, PdfCompliance::PdfA1bUA1);
294 }
295
296 #[test]
297 fn test_xmp_standard_contains_no_compliance_ids() {
298 let xmp = generate_xmp_metadata(Some("Doc"), "fop-rs", PdfCompliance::Standard);
299 assert!(!xmp.contains("pdfaid"));
300 assert!(!xmp.contains("pdfuaid"));
301 }
302
303 #[test]
304 fn test_xmp_metadata_contains_title() {
305 let xmp = generate_xmp_metadata(Some("My Title"), "fop-rs", PdfCompliance::Standard);
306 assert!(xmp.contains("My Title"));
307 }
308
309 #[test]
310 fn test_xmp_metadata_contains_creator_tool() {
311 let xmp = generate_xmp_metadata(None, "fop-render v1.0", PdfCompliance::Standard);
312 assert!(xmp.contains("fop-render v1.0"));
313 }
314
315 #[test]
316 fn test_xmp_metadata_no_title_uses_untitled() {
317 let xmp = generate_xmp_metadata(None, "fop", PdfCompliance::Standard);
318 assert!(xmp.contains("Untitled"));
319 }
320
321 #[test]
322 fn test_xmp_metadata_starts_with_xpacket() {
323 let xmp = generate_xmp_metadata(None, "fop", PdfCompliance::Standard);
324 assert!(xmp.starts_with("<?xpacket"));
325 }
326
327 #[test]
328 fn test_xmp_metadata_ends_with_xpacket() {
329 let xmp = generate_xmp_metadata(None, "fop", PdfCompliance::Standard);
330 assert!(xmp.ends_with("?>"));
331 }
332
333 #[test]
334 fn test_srgb_icc_profile_starts_with_signature() {
335 assert_eq!(
337 &SRGB_ICC_PROFILE[12..16],
338 &[0x6D, 0x6E, 0x74, 0x72],
339 "ICC profile class should be 'mntr'"
340 );
341 }
342
343 #[test]
344 fn test_srgb_icc_profile_colour_space_rgb() {
345 assert_eq!(
347 &SRGB_ICC_PROFILE[16..20],
348 &[0x52, 0x47, 0x42, 0x20],
349 "ICC colour space should be 'RGB '"
350 );
351 }
352
353 #[test]
354 fn test_srgb_icc_profile_pcs_xyz() {
355 assert_eq!(
357 &SRGB_ICC_PROFILE[20..24],
358 &[0x58, 0x59, 0x5A, 0x20],
359 "PCS should be 'XYZ '"
360 );
361 }
362
363 #[test]
364 fn test_compliance_copy_clone() {
365 let c = PdfCompliance::PdfA1b;
366 let c2 = c;
367 assert_eq!(c, c2);
368 }
369}