Skip to main content

gldf_rs/
lib.rs

1//! # gldf_rs
2//! GLDF (Global Lighting Data Format) Library
3//!
4//! **GitHub:** <https://github.com/holg/gldf-rs>
5//!
6//! The GLDF crate provides a set of structures and tools for working with the Global Lighting Data Format (GLDF),
7//! a standardized format for describing lighting products, their characteristics, and technical details.
8//!
9//! GLDF is used in the lighting industry to exchange product information between manufacturers, designers,
10//! and other stakeholders, ensuring consistent representation and interoperability across various software tools.
11//!
12//! This crate offers utilities for serializing and deserializing GLDF data, enabling you to read and write GLDF files
13//! while adhering to the ISO 7127 standard. It also provides helper macros for working with GLDF-specific attributes.
14//!
15//! For more information about GLDF and its specifications, <https::gldf.io> and refer to the ISO 7127 standard.
16//!
17//! # Features
18//!
19//! - Serialize and deserialize GLDF files in compliance with ISO 7127 standard.
20//! - From XML into JSON and vice versa.
21//! - Define GLDF-specific attributes using custom procedural macros.
22//! - Easily work with GLDF data structures and their components.
23//!
24//! For more usage examples and detailed documentation, please refer to the documentation of individual modules and structs.
25//! Most functions are implemented as methods on the struct GldfProduct, which shall represent the Root of the XML structure.
26//! **For more information see : gldf_rs::gldf::GldfProduct**
27//!
28//! [`GldfProduct`]
29//! # Example
30//! ```rust,no_run
31//! use gldf_rs::gldf::GldfProduct;
32//! let loaded: GldfProduct = GldfProduct::load_gldf("tests/data/test.gldf").unwrap();
33//! println!("{:?}", loaded);
34//! // Display pretty printed XML
35//! let x_serialized = loaded.to_xml().unwrap();
36//! println!("{}", x_serialized);
37//! let json_str = loaded.to_json().unwrap();
38//! println!("{}", json_str);
39//! let j_loaded: GldfProduct = GldfProduct::from_json(&json_str).unwrap();
40//! let x_reserialized =  j_loaded.to_xml().unwrap();
41//! println!("{}", x_reserialized);
42//! assert_eq!(x_serialized, x_reserialized);
43//! ```
44//!
45//!
46//! For more information about GLDF and its specifications, refer to the ISO 7127 standard.
47//!
48//! # License
49//!
50//! This project is licensed under the terms of the MIT license.
51
52/// the gldf module (src/gldf/mod.rs)
53pub mod gldf;
54pub use gldf::*;
55
56/// Editable GLDF wrapper for editing and saving GLDF files
57pub mod editable;
58pub use editable::{EditableGldf, EditableGldfStats};
59
60/// Fix-up utilities for legacy / non-conforming GLDF archives.
61pub mod fix;
62pub use fix::fix_legacy_content_types;
63
64/// Validation engine for GLDF files
65pub mod validation;
66pub use validation::{ValidationError, ValidationLevel, ValidationResult};
67
68/// CRUD operations for GldfProduct
69pub mod operations;
70
71/// L3D to LDT mapping utilities
72pub mod mapping;
73pub use mapping::{
74    get_first_l3d_with_ldt, get_l3d_files_with_ldt, get_l3d_ldt_mappings, get_variant_emitter_data,
75    EmitterRenderData, L3dLdtMapping, L3dWithLdt, MountingInfo, MountingType, VariantEmitterData,
76};
77
78/// Conversion utilities for creating GLDF from other formats (LDT/IES)
79#[cfg(feature = "eulumdat")]
80pub mod convert;
81#[cfg(feature = "eulumdat")]
82pub use convert::{ldt_metadata_to_gldf, ldt_to_gldf, LdtMetadata};
83
84/// IFC (Industry Foundation Classes) integration for BIM interoperability
85pub mod ifc;
86pub use ifc::{
87    // IFC Import
88    ifc_to_gldf,
89    import_ifc,
90    GldfGenerator,
91    GldfToIfc,
92    IfcImporter,
93    ImportedLightSource,
94    ImportedLuminaire,
95    ImportedProperties,
96    ImportedVariant,
97    LightEmissionSourceEnum,
98    LightFixtureTypeEnum,
99    StepWriter,
100};
101
102/// Plugin system for embedded WASM viewer plugins
103pub mod plugin;
104pub use plugin::{Plugin, PluginManager, PluginManifest};
105
106#[cfg(test)]
107mod tests;
108
109use anyhow::{Context, Result};
110use regex::Regex;
111use serde_json::from_str as serde_from_str;
112use std::fs::File as StdFile;
113use std::io::Read;
114use std::path::Path;
115use std::path::PathBuf;
116use zip::ZipArchive;
117
118/// Target GLDF schema revision when serializing a `GldfProduct`.
119///
120/// rc.4 introduced backwards-compatible spelling fixes (the corrected
121/// `RatedChromaticityCoordinateValues` element name and `Fluorescent`
122/// enum values), keeping the old forms as deprecated aliases. The
123/// internal model always stores the rc.4-correct form; this enum picks
124/// which spelling lands in the output XML so producers can keep shipping
125/// rc.3-validating files until consumers catch up.
126#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
127pub enum GldfSchemaVersion {
128    /// Output the rc.3 spellings (`RatedChromacityCoordinateValues`,
129    /// `Flourescent Triphosphor`, `Flourescent Halophosphate`). Use this
130    /// for compatibility with consumers pinned to the pre-rc.4 schema.
131    Rc3,
132    /// Output the rc.4-corrected spellings. This is the default and
133    /// validates against both rc.4 and rc.3 (rc.4 keeps the old forms
134    /// as deprecated aliases, but the corrected forms are also accepted
135    /// by older consumers since the deprecation runs the other way).
136    #[default]
137    Rc4,
138}
139
140/// Rewrite rc.4-canonical spellings back to their rc.3 typo'd forms.
141///
142/// The rc.4 schema kept both spellings valid via `<xs:choice>` wrappers
143/// and deprecated-form enum aliases, so this rewrite is needed only for
144/// consumers that haven't bumped to rc.4 yet. Pure string replacement —
145/// the substrings we replace don't appear anywhere except as element
146/// names (`<RatedChromaticityCoordinateValues>` open/close tags) and as
147/// the literal enum values inside `<Cie97LampType>...</Cie97LampType>`
148/// element bodies, so global replace is safe.
149fn downconvert_rc4_to_rc3(xml: &str) -> String {
150    xml
151        // Element name (rc.4 only added this; rc.3 only knew the typo'd form)
152        .replace(
153            "RatedChromaticityCoordinateValues",
154            "RatedChromacityCoordinateValues",
155        )
156        // Cie97LampType enum values
157        .replace("Fluorescent Triphosphor", "Flourescent Triphosphor")
158        .replace("Fluorescent Halophosphate", "Flourescent Halophosphate")
159}
160
161impl GldfProduct {
162    pub fn detach(&mut self) -> anyhow::Result<()> {
163        Ok(())
164    }
165}
166
167#[derive(Clone, Debug, PartialEq)]
168pub struct BufFile {
169    pub name: Option<String>,
170    pub content: Option<Vec<u8>>,
171    pub file_id: Option<String>,
172    pub path: Option<String>,
173}
174
175pub struct FileBufGldf {
176    pub files: Vec<BufFile>,
177    pub gldf: GldfProduct,
178}
179
180/// Implementations for the per se informational GldfProduct struct
181impl GldfProduct {
182    /// Loads a GLDF file from a given path as String and return the XML String of the product.xml file
183    pub fn load_gldf_file_str(&self, path: String) -> anyhow::Result<String> {
184        let zipfile = StdFile::open(Path::new(&self.path))?;
185        let mut zip = ZipArchive::new(zipfile)?;
186        let mut some_str = String::new();
187        let mut some_file = zip.by_name(&path)?;
188        some_file.read_to_string(&mut some_str)?;
189        Ok(some_str)
190    }
191
192    /// Loads a GLDF file from a given path as `Vec<u8>` and return the `Vec<u8>` of the product.xml file
193    /// last but not least for WASM usage.
194    pub fn load_gldf_file(&self, path: String) -> anyhow::Result<Vec<u8>> {
195        let zipfile = StdFile::open(Path::new(&self.path))?;
196        let mut zip = ZipArchive::new(zipfile)?;
197        let mut file_buf = Vec::new();
198        let mut some_file = zip.by_name(&path)?;
199        some_file.read_to_end(&mut file_buf)?;
200        Ok(file_buf)
201    }
202
203    /// a helper function to used by the load_gldf function
204    /// takes a PathBuf and returns the XML String of the product.xml file
205    pub fn get_xml_str_from_gldf(path: PathBuf) -> anyhow::Result<String> {
206        let zipfile = StdFile::open(path)?;
207        let mut zip = ZipArchive::new(zipfile)?;
208        let mut xmlfile = zip.by_name("product.xml")?;
209        let mut xml_str = String::new();
210        xmlfile.read_to_string(&mut xml_str)?;
211        Ok(xml_str)
212    }
213
214    /// a helper function to remove the UTF8 Bom, if present from a given String
215    /// takes a String and returns a String
216    /// needed for some GLDF files, which have BOM in the XML file
217    pub fn remove_bom(s: &str) -> String {
218        if let Some(stripped) = s.strip_prefix("\u{FEFF}") {
219            stripped.to_string()
220        } else {
221            s.to_string()
222        }
223    }
224
225    /// a helper function to sanitize the XML String
226    /// takes a String and returns a String
227    /// GldfProduct does not really care about the XSD version, so we remove it
228    /// and add our own later
229    pub fn sanitize_xml_str(xml_str: &str) -> String {
230        let cleaned_str = Self::remove_bom(xml_str);
231        let re = Regex::new(r"<Root .*?>").unwrap();
232        // well we are lazy for now and simple replace the root element with a generic one
233        let cleaned = re.replace_all(&cleaned_str, "<Root>").to_string();
234        // Upconvert rc.3 typo'd forms to the rc.4 corrected spellings
235        // before the serde-XML pass sees them. The element name is
236        // already covered by `serde(alias = ...)` on the field, so this
237        // step only matters for the enum string values inside
238        // `<Cie97LampType>...</Cie97LampType>` element bodies — those
239        // arrive as plain `String`, no alias machinery applies.
240        cleaned
241            .replace("Flourescent Triphosphor", "Fluorescent Triphosphor")
242            .replace("Flourescent Halophosphate", "Fluorescent Halophosphate")
243    }
244
245    /// a helper function to load a XML String and return the GldfProduct struct
246    pub fn from_xml(xml_str: &str) -> anyhow::Result<GldfProduct> {
247        let my_xml_str = Self::sanitize_xml_str(xml_str);
248        let result: GldfProduct = quick_xml::de::from_str(&my_xml_str)
249            .map_err(|e| anyhow::anyhow!("XML parsing error: {}", e))?;
250        Ok(result)
251    }
252
253    /// Argument the &str path to the GLDF file and return the GldfProduct struct
254    pub fn load_gldf(path: &str) -> anyhow::Result<GldfProduct> {
255        let path_buf = Path::new(path).to_path_buf();
256        let xml_str = GldfProduct::get_xml_str_from_gldf(path_buf)
257            .map_err(anyhow::Error::msg)
258            .context("Failed to parse XML string")?;
259        let mut loaded: GldfProduct = GldfProduct::from_xml(&xml_str)?;
260        loaded.path = path.to_string();
261        Ok(loaded)
262    }
263
264    /// A helper for the WASM, which has the GLDF file as `Vec<u8>` and returns all the files as `Vec<BufFile>`
265    /// which can be later rendered into HTML, e.g. for some GLDF Viewer
266    pub fn load_gldf_from_buf_all(gldf_buf: Vec<u8>) -> anyhow::Result<FileBufGldf> {
267        let zip_buf = std::io::Cursor::new(gldf_buf);
268        let mut zip = ZipArchive::new(zip_buf).context("Failed to open GLDF as ZIP archive")?;
269        let mut file_bufs: Vec<BufFile> = Vec::new();
270        let mut xmlfile = zip
271            .by_name("product.xml")
272            .context("product.xml not found in GLDF archive")?;
273        let mut xml_str = String::new();
274        xmlfile
275            .read_to_string(&mut xml_str)
276            .context("Failed to read product.xml")?;
277        let loaded: GldfProduct =
278            GldfProduct::from_xml(&xml_str).context("Failed to parse product.xml")?;
279        drop(xmlfile);
280
281        for i in 0..zip.len() {
282            if let Ok(mut file) = zip.by_index(i) {
283                if file.is_file() {
284                    let mut buf: Vec<u8> = Vec::new();
285                    if file.read_to_end(&mut buf).is_ok() {
286                        let buf_file = BufFile {
287                            name: Some(file.name().to_string()),
288                            content: Some(buf),
289                            file_id: None,
290                            path: Some(file.name().to_string()),
291                        };
292                        file_bufs.push(buf_file);
293                    }
294                }
295            }
296        }
297        let file_buf = FileBufGldf {
298            files: file_bufs,
299            gldf: loaded,
300        };
301
302        Ok(file_buf)
303    }
304
305    /// A helper function. Argument is the `Vec<u8>` of the GLDF file and returns the GldfProduct struct
306    /// WASM usage e.g.
307    pub fn load_gldf_from_buf(file_buf: Vec<u8>) -> anyhow::Result<GldfProduct> {
308        let zip_buf = std::io::Cursor::new(file_buf);
309        let mut zip = ZipArchive::new(zip_buf).context("Failed to open GLDF as ZIP archive")?;
310        let mut xmlfile = zip
311            .by_name("product.xml")
312            .context("product.xml not found in GLDF archive")?;
313        let mut xml_str = String::new();
314        xmlfile
315            .read_to_string(&mut xml_str)
316            .context("Failed to read product.xml")?;
317        let loaded: GldfProduct =
318            GldfProduct::from_xml(&xml_str).context("Failed to parse product.xml")?;
319        Ok(loaded)
320    }
321
322    /// represent the GldfProduct as JSON String
323    pub fn to_json(&self) -> anyhow::Result<String> {
324        let json_str = serde_json::to_string(&self)?;
325        Ok(json_str)
326    }
327
328    /// represent the GldfProduct as pretty pretty JSON String
329    pub fn to_pretty_json(&self) -> anyhow::Result<String> {
330        let json_str =
331            serde_json::to_string_pretty(&self).context("Failed to serialize to JSON")?;
332        Ok(json_str)
333    }
334
335    /// loads a given JSON &str and returns the GldfProduct struct
336    pub fn from_json(json_str: &str) -> anyhow::Result<GldfProduct> {
337        let j_loaded: GldfProduct = serde_from_str(json_str)?;
338        Ok(j_loaded)
339    }
340
341    /// loads a given JSON file from a PathBuf and returns the GldfProduct struct
342    /// last but not least for WASM usage.
343    pub fn from_json_file(path: PathBuf) -> anyhow::Result<GldfProduct> {
344        let mut json_file = StdFile::open(&path)
345            .with_context(|| format!("Failed to open JSON file: {:?}", path))?;
346        let mut json_str = String::new();
347        json_file
348            .read_to_string(&mut json_str)
349            .context("Failed to read JSON file")?;
350        GldfProduct::from_json(&json_str).context("Failed to parse JSON content")
351    }
352
353    /// Represent the GldfProduct as an XML string targeting the rc.4
354    /// schema (the default). Internal model always stores the rc.4
355    /// canonical form (`RatedChromaticityCoordinateValues`,
356    /// `Fluorescent Triphosphor`, etc.), so the rc.4 path is a straight
357    /// serialize. For rc.3 output use [`Self::to_xml_with_schema`].
358    pub fn to_xml(&self) -> anyhow::Result<String> {
359        self.to_xml_with_schema(GldfSchemaVersion::Rc4)
360    }
361
362    /// Serialize as XML targeting a specific GLDF schema revision.
363    ///
364    /// **rc.4** (default) writes the corrected element names and enum
365    /// values (`RatedChromaticityCoordinateValues`,
366    /// `Fluorescent Triphosphor`, `Fluorescent Halophosphate`).
367    ///
368    /// **rc.3** rewrites those back to the historical typo'd forms
369    /// (`RatedChromacityCoordinateValues`, `Flourescent Triphosphor`,
370    /// `Flourescent Halophosphate`) so files validate against the
371    /// pre-rc.4 XSD that some downstream tools still use. The conversion
372    /// is a pure string-replace pass on the serialized XML — no
373    /// duplicate types or feature flags involved.
374    pub fn to_xml_with_schema(&self, version: GldfSchemaVersion) -> anyhow::Result<String> {
375        let xml_str = quick_xml::se::to_string(&self)?;
376        let xml_str = match version {
377            GldfSchemaVersion::Rc4 => xml_str,
378            GldfSchemaVersion::Rc3 => downconvert_rc4_to_rc3(&xml_str),
379        };
380        Ok(format!(
381            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n{}",
382            xml_str
383        ))
384    }
385
386    /// returns the photometric files as Vec<&File>
387    pub fn get_phot_files(&self) -> anyhow::Result<Vec<&File>> {
388        let mut result: Vec<&File> = Vec::new();
389        for f in self.general_definitions.files.file.iter() {
390            let content_type = &f.content_type;
391            if content_type.starts_with("ldc") {
392                result.push(f)
393            }
394        }
395        Ok(result.to_owned())
396    }
397
398    /// returns the image files as Vec<&File>
399    pub fn get_image_def_files(&self) -> Result<Vec<&File>> {
400        let mut result: Vec<&File> = Vec::new();
401        for f in self.general_definitions.files.file.iter() {
402            let content_type = &f.content_type;
403            if content_type.starts_with("image") {
404                result.push(f)
405            }
406        }
407        Ok(result.to_owned())
408    }
409
410    /// returns the image files as Vec<&File>
411    pub fn get_image_zip_files(&self) -> anyhow::Result<Vec<&File>> {
412        let mut result: Vec<&File> = Vec::new();
413        for f in self.general_definitions.files.file.iter() {
414            let content_type = &f.content_type;
415            if content_type.starts_with("image") {
416                result.push(f)
417            }
418        }
419        Ok(result.to_owned())
420    }
421
422    /// from the given file_id of the ldc file reference, return the ldc file as String
423    /// it could as well be the type_attr "url", which will be fetched from the web first
424    /// overridden for the WASM portage, so not used for WASM portage
425    #[cfg(feature = "http")]
426    pub async fn get_ldc_by_id(&self, file_id: String) -> Result<String, anyhow::Error> {
427        let mut result: String = "".to_owned();
428        for f in self.general_definitions.files.file.iter() {
429            if f.id == file_id {
430                let mut ldc_path = "ldc/".to_owned();
431                let file_name = f.file_name.to_owned();
432                if f.type_attr == "url" {
433                    let tmp = fetch_text_from_url(&file_name).await?;
434                    result.push_str(tmp.as_str());
435                } else {
436                    ldc_path.push_str(&file_name);
437                    result.push_str(&self.load_gldf_file_str(ldc_path)?);
438                }
439            }
440        }
441        Ok(result)
442    }
443
444    /// from the given file_id of the ldc file reference, return the ldc file as String
445    /// (non-HTTP version - does not support URL type files)
446    #[cfg(not(feature = "http"))]
447    pub fn get_ldc_by_id(&self, file_id: String) -> Result<String, anyhow::Error> {
448        let mut result: String = "".to_owned();
449        for f in self.general_definitions.files.file.iter() {
450            if f.id == file_id {
451                if f.type_attr == "url" {
452                    return Err(anyhow::anyhow!(
453                        "URL file type not supported without http feature"
454                    ));
455                }
456                let mut ldc_path = "ldc/".to_owned();
457                ldc_path.push_str(&f.file_name);
458                result.push_str(&self.load_gldf_file_str(ldc_path)?);
459            }
460        }
461        Ok(result)
462    }
463
464    /// Gets all the file definitions as `Vec<File>`
465    pub fn get_all_file_definitions(&self) -> anyhow::Result<Vec<File>> {
466        let mut result: Vec<File> = Vec::new();
467        for f in self.general_definitions.files.file.iter() {
468            result.push(f.to_owned());
469        }
470        Ok(result)
471    }
472
473    /// Gets all the file definitions which are of content_type url as `Vec<File>`
474    pub fn get_url_file_definitions(&self) -> anyhow::Result<Vec<File>> {
475        let mut result: Vec<File> = Vec::new();
476        for f in self.general_definitions.files.file.iter() {
477            if f.content_type == "url" {
478                result.push(f.to_owned());
479            }
480        }
481        Ok(result)
482    }
483}
484
485/// helper function to get the content of the url as File from the given url
486#[cfg(feature = "http")]
487pub async fn fetch_text_from_url(url: &str) -> Result<String, reqwest::Error> {
488    let response = reqwest::get(url).await?;
489    let text = response.text().await?;
490    Ok(text)
491}
492
493#[cfg(feature = "http")]
494pub async fn fetch_content_from_url(url: &str) -> Result<Vec<u8>, reqwest::Error> {
495    let response = reqwest::get(url).await?;
496    let content = response.bytes().await?;
497    Ok(content.to_vec())
498}