├── .gitignore ├── data └── test_lron ├── .github └── workflows │ └── rust.yml ├── src ├── lrobject.rs ├── fromdb.rs ├── libraryfiles.rs ├── keywords.rs ├── lib.rs ├── keywordtree.rs ├── collections.rs ├── content.rs ├── folders.rs ├── lron.rs ├── catalog.rs ├── images.rs └── bin │ └── dumper.rs ├── Cargo.toml ├── examples └── lron_dump.rs ├── NEWS ├── README ├── doc └── lrcat_format.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /data/test_lron: -------------------------------------------------------------------------------- 1 | s = { 2 | { 3 | criteria = "rating", 4 | operation = ">", 5 | value = 0, 6 | value2 = 0, 7 | someOther = "anObject = {\ 8 | key = \"lr\",\ 9 | }\ 10 | ", 11 | }, 12 | combine = "intersect", 13 | } -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Clippy 22 | run: cargo clippy --all-targets 23 | - name: Run tests 24 | run: cargo test --verbose 25 | - name: Format 26 | run: cargo fmt --check 27 | -------------------------------------------------------------------------------- /src/lrobject.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | /// Lightroom local id Used as a catalog globel identifier. Values 8 | /// seems to be unique accross types. 9 | pub type LrId = i64; 10 | 11 | /// Basic object from the catalog. 12 | /// `Collection` as an exception 13 | pub trait LrObject { 14 | /// The local id 15 | fn id(&self) -> LrId; 16 | /// The global id. A valid UUID. Doesn't seem to be used anywhere 17 | /// though. 18 | fn uuid(&self) -> &str; 19 | } 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lrcat-extractor" 3 | version = "0.5.0" 4 | authors = ["Hubert Figuière "] 5 | edition = "2018" 6 | description = "Extract data from Adobe Lightroom™ catalogs." 7 | license = "MPL-2.0" 8 | repository = "https://github.com/hfiguiere/lrcat-extractor" 9 | documentation = "https://docs.rs/lrcat-extractor/" 10 | 11 | [dependencies] 12 | chrono = "0.4.0" 13 | peg = "0.8.3" 14 | rusqlite = "0.37" 15 | thiserror = "2" 16 | 17 | clap = { version = "^4.5", optional = true, features = ["derive"] } 18 | 19 | [lib] 20 | name = "lrcat" 21 | 22 | [[bin]] 23 | name = "dumper" 24 | required-features = ["binaries"] 25 | 26 | [features] 27 | default = ["binaries"] 28 | binaries = ["clap"] 29 | -------------------------------------------------------------------------------- /src/fromdb.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::Row; 8 | 9 | use crate::catalog::CatalogVersion; 10 | 11 | /// Trait to define loading from a database. 12 | pub trait FromDb: Sized { 13 | /// Read one element from a database Row obtained through a query 14 | /// build with the tables and columns provided. 15 | /// The version of the catalog allow selecting the proper variant. 16 | fn read_from(version: CatalogVersion, row: &Row) -> crate::Result; 17 | /// DB tables used in select query. 18 | fn read_db_tables(version: CatalogVersion) -> &'static str; 19 | /// DB columns used in select query. 20 | fn read_db_columns(version: CatalogVersion) -> &'static str; 21 | /// WHERE clause for joining tables (doesn't include `WHERE`) 22 | /// Default is empty 23 | fn read_join_where(_version: CatalogVersion) -> &'static str { 24 | "" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/lron_dump.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | //! Dump lron file. Allow validating the grammar. 8 | 9 | extern crate lrcat; 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Read; 14 | 15 | /// Parse the lron file and output it's parsed content or an error. 16 | /// @return an error in case of IO error. 17 | fn dump_lron(filename: &str) -> std::io::Result<()> { 18 | let mut file = File::open(filename).expect("Unknown file"); 19 | 20 | let mut buffer = String::new(); 21 | file.read_to_string(&mut buffer)?; 22 | 23 | let o = lrcat::lron::Object::from_string(&buffer); 24 | match o { 25 | Ok(ref o) => { 26 | println!("Result: {:?}", o); 27 | } 28 | Err(e) => println!("Error parsing file {}: {:?}", filename, e), 29 | } 30 | Ok(()) 31 | } 32 | 33 | fn main() { 34 | let args: Vec = env::args().collect(); 35 | if args.len() < 2 { 36 | return; 37 | } 38 | 39 | let mut iter = args.iter(); 40 | iter.next(); 41 | for filename in iter { 42 | if let Err(err) = dump_lron(filename) { 43 | println!("Error dumping lron: {} {}", filename, err); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | 0.5.0 - 26 July 2025 2 | 3 | - dumper: Use clap instead of docopt. 4 | - dumper: Added `list` command. 5 | - clap is only build optionally. 6 | - API: use `thiserror`. 7 | - API: Catalog::open() returns a Result<> now. 8 | - Update rusqlite to 0.37 9 | 10 | 0.4.1 - 2 September 2023 11 | 12 | - Support escaped strings: lots of case where the value is a string 13 | that deserialize too, and caused parsing failures. 14 | 15 | 0.4.0 - 1 September 2023 16 | 17 | - Update rusqlite to 0.29.0. 18 | - Update docopt to 1.1.1. 19 | - Use re-exported serde derive. 20 | - Documentation improvements. 21 | - Implement `PartialEq` for public types where appropriate. 22 | 23 | 0.3.0 - 4 November 2022 24 | 25 | - Fix some clippy warnings 26 | - Update rusqlite to 0.28.0 27 | 28 | 0.2.1 - 30 December 2021 29 | 30 | - Added `Clone` derive to most type. 31 | - Dumper splits folders and root folders. 32 | - Folders::find_root_folder() is public 33 | 34 | 0.2.0 - 9 December 2021 35 | 36 | - Added `Catalog::images_for_collection()` to get the list of images 37 | belonging to a collection. 38 | - Allow immutable access to Catalog object once loaded: 39 | - `Catalog::images()` 40 | - `Catalog::keywords()` 41 | - `Catalog::folders()` 42 | - `Catalog::libfiles()` 43 | - `Catalog::collections()` 44 | - Possible support for Lr3 catalogs (largely untested). 45 | - Performance improvements. 46 | 47 | 0.1.0 - 12 November 2021 48 | 49 | - Initial release -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | lrcat extractor 2 | =============== 3 | 4 | Extract the data from Adobe Lightroom catalogs (currently version 2, 4 5 | and 6) in order to facilitate importing it into another application. 6 | 7 | This is written in Rust. 8 | 9 | Requires: 10 | - Rust (1.29) 11 | - cargo to build 12 | 13 | The is a crate (library) meant to be used by other applications. Its 14 | goal is to abstract the format away into an API that can be used to 15 | import into an application. 16 | 17 | Building 18 | -------- 19 | 20 | This is meant to be used by another application, so you need to add 21 | to your `Cargo.toml` the crate: 22 | 23 | `lrcat-extractor = 0.2.0` 24 | 25 | To build the crate, simply: 26 | 27 | $ cargo build 28 | 29 | The crate comes with a dumper utility, that's used mostly for 30 | debugging. It is hidden behind a "feature". 31 | 32 | $ cargo run --features=binaries 33 | 34 | Using 35 | ----- 36 | 37 | ```Rust 38 | let mut catalog = Catalog::new(&path_to_catalog); 39 | if catalog.open() { 40 | // check the catalog is the right version 41 | if !catalog.catalog_version.is_supported() { 42 | println!("Unsupported catalog version"); 43 | return; 44 | } 45 | } 46 | ``` 47 | 48 | See the documentation hosted at https://docs.rs/lrcat-extractor/ 49 | 50 | You can also use `cargo doc` to generate it locally. 51 | 52 | License 53 | ------- 54 | 55 | This Source Code Form is subject to the terms of the Mozilla Public 56 | License, v. 2.0. If a copy of the MPL was not distributed with this 57 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 58 | 59 | See the LICENSE file in this repository. 60 | 61 | Maintainer: 62 | Hubert Figuière -------------------------------------------------------------------------------- /src/libraryfiles.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::Row; 8 | 9 | use crate::catalog::CatalogVersion; 10 | use crate::fromdb::FromDb; 11 | use crate::lrobject::{LrId, LrObject}; 12 | 13 | /// Define a backing file in the `Catalog`. `Images` are 14 | /// connected to one. 15 | pub struct LibraryFile { 16 | id: LrId, 17 | uuid: String, 18 | /// Basename (no extension) of the file 19 | pub basename: String, 20 | /// Extension of the file 21 | pub extension: String, 22 | /// `Folder` id containing file 23 | pub folder: LrId, 24 | /// Extensions of the sidecar(s), comma separated. 25 | pub sidecar_extensions: String, 26 | } 27 | 28 | impl LrObject for LibraryFile { 29 | fn id(&self) -> LrId { 30 | self.id 31 | } 32 | fn uuid(&self) -> &str { 33 | &self.uuid 34 | } 35 | } 36 | 37 | impl FromDb for LibraryFile { 38 | fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result { 39 | Ok(LibraryFile { 40 | id: row.get(0)?, 41 | uuid: row.get(1)?, 42 | basename: row.get(2)?, 43 | extension: row.get(3)?, 44 | folder: row.get(4)?, 45 | sidecar_extensions: row.get(5)?, 46 | }) 47 | } 48 | fn read_db_tables(_version: CatalogVersion) -> &'static str { 49 | "AgLibraryFile" 50 | } 51 | fn read_db_columns(_version: CatalogVersion) -> &'static str { 52 | "id_local,id_global,baseName,extension,folder,sidecarExtensions" 53 | } 54 | } 55 | 56 | impl LibraryFile {} 57 | -------------------------------------------------------------------------------- /src/keywords.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::Row; 8 | 9 | use crate::catalog::CatalogVersion; 10 | use crate::fromdb::FromDb; 11 | use crate::lrobject::{LrId, LrObject}; 12 | 13 | /// A Lightroom keyword. 14 | #[derive(Clone)] 15 | pub struct Keyword { 16 | /// Local id 17 | id: LrId, 18 | /// Global UUID 19 | uuid: String, 20 | // date_created: DateTime, 21 | /// the actual keyword 22 | pub name: String, 23 | /// The parent. For top-level the value is `Catalog::root_keyword_id` 24 | pub parent: LrId, 25 | } 26 | 27 | impl LrObject for Keyword { 28 | fn id(&self) -> LrId { 29 | self.id 30 | } 31 | fn uuid(&self) -> &str { 32 | &self.uuid 33 | } 34 | } 35 | 36 | impl FromDb for Keyword { 37 | fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result { 38 | let name = row.get(3).ok(); 39 | let parent = row.get(4).ok(); 40 | Ok(Keyword { 41 | id: row.get(0)?, 42 | uuid: row.get(1)?, 43 | name: name.unwrap_or_default(), 44 | parent: parent.unwrap_or(0), 45 | }) 46 | } 47 | 48 | fn read_db_tables(_version: CatalogVersion) -> &'static str { 49 | "AgLibraryKeyword" 50 | } 51 | 52 | fn read_db_columns(_version: CatalogVersion) -> &'static str { 53 | "id_local,id_global,cast(dateCreated as text),name,parent" 54 | } 55 | } 56 | 57 | impl Keyword { 58 | /// Initialize a new keyword. 59 | pub fn new(id: LrId, parent: LrId, uuid: &str, name: &str) -> Keyword { 60 | Keyword { 61 | id, 62 | parent, 63 | uuid: String::from(uuid), 64 | name: String::from(name), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | extern crate chrono; 8 | extern crate peg; 9 | extern crate rusqlite; 10 | 11 | mod catalog; 12 | mod collections; 13 | mod content; 14 | mod folders; 15 | mod fromdb; 16 | mod images; 17 | mod keywords; 18 | mod keywordtree; 19 | mod libraryfiles; 20 | mod lrobject; 21 | pub mod lron; 22 | 23 | /// Point 24 | #[derive(Debug, PartialEq)] 25 | pub struct Point { 26 | pub x: f64, 27 | pub y: f64, 28 | } 29 | 30 | /// Aspect ratio. 31 | #[derive(Debug, PartialEq)] 32 | pub struct AspectRatio { 33 | pub width: i32, 34 | pub height: i32, 35 | } 36 | 37 | /// Rectangle. Lr uses 0..1.0 for 38 | /// crops rectangles. 39 | #[derive(Debug, PartialEq)] 40 | pub struct Rect { 41 | pub top: f64, 42 | pub bottom: f64, 43 | pub left: f64, 44 | pub right: f64, 45 | } 46 | 47 | /// Error from the crate, agreggate with sqlite errors. 48 | #[derive(Debug, thiserror::Error)] 49 | pub enum Error { 50 | #[error("Unimplemented")] 51 | /// Unimplemented 52 | Unimplemented, 53 | #[error("LrCat: Skip.")] 54 | /// Skip the item (when reading from Db) 55 | Skip, 56 | #[error("LrCat: Unsupported catalog version.")] 57 | /// Unsupported catalog version 58 | UnsupportedVersion, 59 | #[error("LrCat: SQL error: {0}")] 60 | /// Sql Error 61 | Sql(#[from] rusqlite::Error), 62 | #[error("LrCat: Lron parsing error: {0}")] 63 | /// Lron parsing error 64 | Lron(#[from] peg::error::ParseError), 65 | } 66 | 67 | /// Result type for the crate. 68 | pub type Result = std::result::Result; 69 | 70 | pub use catalog::{Catalog, CatalogVersion}; 71 | pub use collections::Collection; 72 | pub use content::Content; 73 | pub use folders::{Folder, Folders, RootFolder}; 74 | pub use images::Image; 75 | pub use keywords::Keyword; 76 | pub use keywordtree::KeywordTree; 77 | pub use libraryfiles::LibraryFile; 78 | pub use lrobject::{LrId, LrObject}; 79 | -------------------------------------------------------------------------------- /src/keywordtree.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use std::collections::{BTreeMap, HashMap}; 8 | 9 | use super::keywords::Keyword; 10 | use super::lrobject::LrObject; 11 | 12 | /// Keyword tree 13 | /// Operate as a hash multimap of parent -> `Vec` 14 | #[derive(Default)] 15 | pub struct KeywordTree { 16 | // HashMap. Key is the parent id. Values: the children ids. 17 | map: HashMap>, 18 | } 19 | 20 | impl KeywordTree { 21 | pub fn new() -> KeywordTree { 22 | KeywordTree::default() 23 | } 24 | 25 | /// Get children for keyword with `id` 26 | pub fn children_for(&self, id: i64) -> Vec { 27 | if let Some(children) = self.map.get(&id) { 28 | return children.clone(); 29 | } 30 | vec![] 31 | } 32 | 33 | fn add_child(&mut self, keyword: &Keyword) { 34 | self.map.entry(keyword.parent).or_default(); 35 | self.map 36 | .get_mut(&keyword.parent) 37 | .unwrap() 38 | .push(keyword.id()); 39 | } 40 | 41 | /// Add children to the tree node. 42 | pub fn add_children(&mut self, children: &BTreeMap) { 43 | for child in children.values() { 44 | self.add_child(child); 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | pub fn test() { 50 | let mut keywords: BTreeMap = BTreeMap::new(); 51 | keywords.insert(1, Keyword::new(1, 0, "", "")); 52 | keywords.insert(2, Keyword::new(2, 1, "", "")); 53 | keywords.insert(3, Keyword::new(3, 2, "", "")); 54 | keywords.insert(4, Keyword::new(4, 0, "", "")); 55 | keywords.insert(5, Keyword::new(5, 2, "", "")); 56 | 57 | let mut tree = KeywordTree::new(); 58 | tree.add_children(&keywords); 59 | 60 | assert_eq!(tree.map.len(), 3); 61 | assert_eq!(tree.map[&0].len(), 2); 62 | assert_eq!(tree.map[&1].len(), 1); 63 | assert_eq!(tree.map[&2].len(), 2); 64 | 65 | let children = tree.children_for(0); 66 | assert_eq!(children, vec![1, 4]); 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | #[test] 72 | fn keyword_tree_test() { 73 | KeywordTree::test(); 74 | } 75 | -------------------------------------------------------------------------------- /src/collections.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::{Connection, Row}; 8 | 9 | use crate::catalog::CatalogVersion; 10 | use crate::content::Content; 11 | use crate::fromdb::FromDb; 12 | use crate::lrobject::LrId; 13 | 14 | /// A collection as defined in Lightroom 15 | pub struct Collection { 16 | /// Local id of the collection 17 | id: LrId, 18 | /// Name of the collection (displayed in the UI) 19 | pub name: String, 20 | /// Parent of the `Collection` 21 | pub parent: LrId, 22 | /// is system only (seems to be the Quick Pick collection) 23 | pub system_only: bool, 24 | /// content definition of the collection 25 | pub content: Option, 26 | } 27 | 28 | impl FromDb for Collection { 29 | fn read_from(version: CatalogVersion, row: &Row) -> crate::Result { 30 | match version { 31 | CatalogVersion::Lr4 | CatalogVersion::Lr6 => Ok(Collection { 32 | id: row.get(0)?, 33 | name: row.get(2)?, 34 | parent: row.get(3).unwrap_or(0), 35 | system_only: !matches!(row.get::(4)? as i64, 0), 36 | content: None, 37 | }), 38 | CatalogVersion::Lr2 => { 39 | let tag_type: Box = row.get(3)?; 40 | let name: String = row.get(1).unwrap_or_else(|_| { 41 | if tag_type.as_ref() == "AgQuickCollectionTagKind" { 42 | "Quick Collection" 43 | } else { 44 | "" 45 | } 46 | .to_owned() 47 | }); 48 | match tag_type.as_ref() { 49 | "AgQuickCollectionTagKind" | "AgCollectionTagKind" => Ok(Collection { 50 | id: row.get(0)?, 51 | name, 52 | parent: row.get(2).unwrap_or(0), 53 | system_only: matches!(tag_type.as_ref(), "AgQuickCollectionTagKind"), 54 | content: None, 55 | }), 56 | _ => Err(crate::Error::Skip), 57 | } 58 | } 59 | _ => Err(crate::Error::UnsupportedVersion), 60 | } 61 | } 62 | 63 | fn read_db_tables(version: CatalogVersion) -> &'static str { 64 | match version { 65 | CatalogVersion::Lr4 | CatalogVersion::Lr6 => "AgLibraryCollection", 66 | CatalogVersion::Lr2 => "AgLibraryTag", 67 | _ => "", 68 | } 69 | } 70 | 71 | fn read_db_columns(version: CatalogVersion) -> &'static str { 72 | match version { 73 | CatalogVersion::Lr4 | CatalogVersion::Lr6 => { 74 | "id_local,genealogy,name,parent,systemOnly" 75 | } 76 | CatalogVersion::Lr2 => "id_local,name,parent,kindName", 77 | _ => "", 78 | } 79 | } 80 | } 81 | 82 | impl Collection { 83 | /// Return the local_id of the collection. 84 | pub fn id(&self) -> LrId { 85 | self.id 86 | } 87 | 88 | /// Read the `content` for this collection from the database. 89 | pub fn read_content(&self, conn: &Connection) -> Content { 90 | Content::from_db(conn, "AgLibraryCollectionContent", "collection", self.id) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/content.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use std::fmt; 8 | 9 | use rusqlite::Connection; 10 | 11 | use crate::lron; 12 | 13 | #[derive(Clone, Copy, Debug, PartialEq)] 14 | /// Sorting direction 15 | pub enum SortDirection { 16 | /// Ascending sort 17 | Ascending, 18 | /// Descending sort 19 | Descending, 20 | /// Unknown value 21 | Unknown, 22 | } 23 | 24 | /// Represent the content view. Applies to `Collection` and `Folder` 25 | #[derive(Default, Clone)] 26 | pub struct Content { 27 | /// Filter 28 | pub filter: Option, 29 | /// What to sort on 30 | pub sort_type: Option, 31 | /// Which direction to sort 32 | pub sort_direction: Option, 33 | /// Define the smart collection (if any) 34 | pub smart_collection: Option, 35 | } 36 | 37 | impl fmt::Debug for Content { 38 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 39 | let mut empty: bool = true; 40 | if let Some(ref filter) = self.filter { 41 | write!(f, "filter: {filter:?}")?; 42 | empty = false; 43 | } 44 | if let Some(ref sort_type) = self.sort_type { 45 | if !empty { 46 | write!(f, ", ")?; 47 | } 48 | write!(f, "sort: {sort_type:?}")?; 49 | empty = false; 50 | } 51 | if let Some(ref direction) = self.sort_direction { 52 | if !empty { 53 | write!(f, ", ")?; 54 | } 55 | write!(f, "direction: {direction:?}")?; 56 | empty = false; 57 | } 58 | if let Some(ref smart_coll) = self.smart_collection { 59 | if !empty { 60 | write!(f, ", ")?; 61 | } 62 | write!(f, "smart_collection: {smart_coll:?}")?; 63 | } 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl Content { 69 | pub fn from_db( 70 | conn: &Connection, 71 | table: &str, 72 | container_col: &str, 73 | container_id: i64, 74 | ) -> Content { 75 | let mut content = Content::default(); 76 | 77 | let query = format!("SELECT content, owningModule from {table} where {container_col}=?1",); 78 | if let Ok(mut stmt) = conn.prepare(&query) { 79 | let mut rows = stmt.query([&container_id]).unwrap(); 80 | while let Ok(Some(row)) = rows.next() { 81 | // We ignore the result. 82 | // XXX shall we display a warning on error? likely 83 | let _ = row.get(1).map(|owning_module: String| { 84 | let value = row.get(0); 85 | match owning_module.as_str() { 86 | "com.adobe.ag.library.filter" => content.filter = value.ok(), 87 | "com.adobe.ag.library.sortType" => content.sort_type = value.ok(), 88 | "com.adobe.ag.library.sortDirection" => { 89 | content.sort_direction = if let Ok(sd) = value { 90 | match sd.as_str() { 91 | "ascending" => Some(SortDirection::Ascending), 92 | "descending" => Some(SortDirection::Descending), 93 | _ => Some(SortDirection::Unknown), 94 | } 95 | } else { 96 | None 97 | } 98 | } 99 | "ag.library.smart_collection" => { 100 | if let Ok(ref sc) = value { 101 | content.smart_collection = lron::Object::from_string(sc).ok(); 102 | } 103 | } 104 | _ => (), 105 | }; 106 | }); 107 | } 108 | } 109 | content 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/folders.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::{Connection, Row}; 8 | 9 | use crate::catalog::CatalogVersion; 10 | use crate::content::Content; 11 | use crate::fromdb::FromDb; 12 | use crate::lrobject::{LrId, LrObject}; 13 | 14 | /// A folder define the container for `LibraryFiles` 15 | /// They are all attached to a `RootFolder` 16 | #[derive(Clone)] 17 | pub struct Folder { 18 | id: LrId, 19 | uuid: String, 20 | /// Path from the `RootFolder` 21 | pub path_from_root: String, 22 | /// Id of the `RootFolder` 23 | pub root_folder: LrId, 24 | pub content: Option, 25 | } 26 | 27 | impl LrObject for Folder { 28 | fn id(&self) -> LrId { 29 | self.id 30 | } 31 | fn uuid(&self) -> &str { 32 | &self.uuid 33 | } 34 | } 35 | 36 | impl FromDb for Folder { 37 | fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result { 38 | Ok(Folder { 39 | id: row.get(0)?, 40 | uuid: row.get(1)?, 41 | path_from_root: row.get(2)?, 42 | root_folder: row.get(3)?, 43 | content: None, 44 | }) 45 | } 46 | fn read_db_tables(_version: CatalogVersion) -> &'static str { 47 | "AgLibraryFolder" 48 | } 49 | fn read_db_columns(_version: CatalogVersion) -> &'static str { 50 | "id_local,id_global,pathFromRoot,rootFolder" 51 | } 52 | } 53 | 54 | impl Folder { 55 | pub fn new(id: LrId, uuid: &str) -> Folder { 56 | Folder { 57 | id, 58 | uuid: String::from(uuid), 59 | path_from_root: String::from(""), 60 | root_folder: 0, 61 | content: None, 62 | } 63 | } 64 | pub fn read_content(&self, conn: &Connection) -> Content { 65 | Content::from_db(conn, "AgFolderContent", "containingFolder", self.id) 66 | } 67 | } 68 | 69 | /// Represent the ancestor of `Folder` and map to 70 | /// an absolute path 71 | #[derive(Clone)] 72 | pub struct RootFolder { 73 | id: LrId, 74 | uuid: String, 75 | /// Absolute path of the `RootFolder` 76 | pub absolute_path: String, 77 | /// (User readable) name of the `RootFolder` 78 | pub name: String, 79 | /// Eventually if it is possible the path is relative 80 | /// to the catalog file. 81 | pub relative_path_from_catalog: Option, 82 | } 83 | 84 | impl LrObject for RootFolder { 85 | fn id(&self) -> LrId { 86 | self.id 87 | } 88 | fn uuid(&self) -> &str { 89 | &self.uuid 90 | } 91 | } 92 | 93 | impl RootFolder { 94 | /// Create a new `RootFolder` with an id and uuid 95 | pub fn new(id: LrId, uuid: &str) -> RootFolder { 96 | RootFolder { 97 | id, 98 | uuid: String::from(uuid), 99 | absolute_path: String::from(""), 100 | name: String::from(""), 101 | relative_path_from_catalog: None, 102 | } 103 | } 104 | } 105 | 106 | impl FromDb for RootFolder { 107 | fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result { 108 | Ok(RootFolder { 109 | id: row.get(0)?, 110 | uuid: row.get(1)?, 111 | absolute_path: row.get(2)?, 112 | name: row.get(3)?, 113 | relative_path_from_catalog: row.get(4).ok(), 114 | }) 115 | } 116 | 117 | fn read_db_tables(_version: CatalogVersion) -> &'static str { 118 | "AgLibraryRootFolder" 119 | } 120 | 121 | fn read_db_columns(_version: CatalogVersion) -> &'static str { 122 | "id_local,id_global,absolutePath,name,relativePathFromCatalog" 123 | } 124 | } 125 | 126 | /// Represent all the folders 127 | #[derive(Clone, Default)] 128 | pub struct Folders { 129 | /// The `RootFolder` list 130 | pub roots: Vec, 131 | /// The `Folder` list 132 | pub folders: Vec, 133 | } 134 | 135 | impl Folders { 136 | pub fn new() -> Folders { 137 | Folders::default() 138 | } 139 | 140 | /// Return `true` is it is empty 141 | pub fn is_empty(&self) -> bool { 142 | self.roots.is_empty() && self.folders.is_empty() 143 | } 144 | 145 | /// Add a `Folder` 146 | pub fn add_folder(&mut self, folder: Folder) { 147 | self.folders.push(folder); 148 | } 149 | 150 | /// Add a `RootFolder` 151 | pub fn add_root_folder(&mut self, root_folder: RootFolder) { 152 | self.roots.push(root_folder); 153 | } 154 | 155 | /// Append a vector of `Folder` 156 | pub fn append_folders(&mut self, mut folders: Vec) { 157 | self.folders.append(&mut folders); 158 | } 159 | 160 | /// Append a vector of `RootFolder` 161 | pub fn append_root_folders(&mut self, mut root_folders: Vec) { 162 | self.roots.append(&mut root_folders); 163 | } 164 | 165 | /// Return the eventual `RootFolder` with the id. 166 | pub fn find_root_folder(&self, id: LrId) -> Option<&RootFolder> { 167 | self.roots.iter().find(|&root| root.id() == id) 168 | } 169 | 170 | /// Resolve the folder path by providing an absolute path 171 | /// This does not check if the path exist but merely combine 172 | /// the `RootFolder` absolute_path and the `Folder` relative path 173 | pub fn resolve_folder_path(&self, folder: &Folder) -> Option { 174 | let root_folder = self.find_root_folder(folder.root_folder)?; 175 | let mut root_path = root_folder.absolute_path.clone(); 176 | root_path += &folder.path_from_root; 177 | Some(root_path) 178 | } 179 | } 180 | 181 | #[cfg(test)] 182 | #[test] 183 | fn test_resolve_folder_path() { 184 | let mut folders = Folders::new(); 185 | 186 | let mut rfolder = RootFolder::new(24, "toplevel"); 187 | rfolder.absolute_path = String::from("/home/hub/Pictures"); 188 | rfolder.name = String::from("Pictures"); 189 | folders.add_root_folder(rfolder); 190 | 191 | let mut folder = Folder::new(42, "foobar"); 192 | folder.root_folder = 24; 193 | folder.path_from_root = String::from("/2017/10"); 194 | folders.add_folder(folder); 195 | 196 | let resolved = folders.resolve_folder_path(&folders.folders[0]); 197 | assert!(resolved.is_some()); 198 | let resolved = resolved.unwrap(); 199 | assert_eq!(resolved, "/home/hub/Pictures/2017/10"); 200 | } 201 | -------------------------------------------------------------------------------- /src/lron.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | //! lron stands for Lightroom Object Notation, specific to Lightroom 8 | //! that is found throughout the catalog database to store arbitrary 9 | //! but structured data. 10 | //! 11 | //! lron looks like plist (before XML) or JSON, but doesn't match 12 | //! either syntax. 13 | //! 14 | //! Note: I couldn't figure out what this format was called, so I 15 | //! couldn't reuse an existing parser. If you have a better idea, 16 | //! please, let me know. 17 | //! 18 | //! Note2: The crate [`agprefs`](https://crates.io/crates/agprefs) 19 | //! call it `agprefs`. 20 | //! 21 | //! It has the form 22 | //! ```json 23 | //! name = { 24 | //! object = { 25 | //! x = 1.3, 26 | //! string = "some text", 27 | //! }, 28 | //! } 29 | //! ``` 30 | //! 31 | //! The text is parsed using peg. 32 | //! 33 | //! You obtain the expression from the text by the following: 34 | //! ``` 35 | //! use lrcat::lron; 36 | //! 37 | //! let lron_text = "name = {}"; // load the text in the string 38 | //! 39 | //! if let Ok(object) = lron::Object::from_string(lron_text) { 40 | //! // do your stuff with it 41 | //! } 42 | //! ``` 43 | 44 | /// Lron Value 45 | #[derive(Clone, Debug, PartialEq)] 46 | pub enum Value { 47 | Dict(Vec), 48 | Str(String), 49 | ZStr(String), 50 | Int(i32), 51 | Float(f64), 52 | Bool(bool), 53 | } 54 | 55 | impl Value { 56 | /// Try to convert the value into a number of type T. This is 57 | /// because number are untyped in Lron, and the parser will manage 58 | /// float or int. Instead of having a generic Number type, it's 59 | /// better this way. 60 | pub fn to_number(&self) -> Option 61 | where 62 | T: std::convert::From + std::convert::From, 63 | { 64 | match *self { 65 | Self::Int(i) => Some(i.into()), 66 | Self::Float(f) => Some(f.into()), 67 | _ => None, 68 | } 69 | } 70 | } 71 | 72 | /// A key/value pair. 73 | #[derive(Clone, Debug, PartialEq)] 74 | pub struct Pair { 75 | pub key: String, 76 | pub value: Value, 77 | } 78 | 79 | /// Lron Object 80 | #[derive(Clone, Debug, PartialEq)] 81 | pub enum Object { 82 | Dict(Vec), 83 | Pair(Pair), 84 | Str(String), 85 | ZStr(String), 86 | Int(i32), 87 | } 88 | 89 | /// Alias result type for parsing a Lron object. 90 | type Result = std::result::Result>; 91 | 92 | impl Object { 93 | /// Create an object from a string 94 | pub fn from_string(s: &str) -> Result { 95 | lron::root(s) 96 | } 97 | } 98 | 99 | // lron stand for Lightroom Object Notation 100 | // Some sort of JSON specific to Lightroom 101 | // 102 | // lron data syntax is defined in this PEG grammar. 103 | peg::parser! {grammar lron() for str { 104 | 105 | use std::str::FromStr; 106 | 107 | pub rule root() -> Object 108 | = key:identifier() _() "=" _() value:array() _() 109 | { Object::Pair(Pair{key, value: Value::Dict(value)}) } 110 | 111 | rule array() -> Vec 112 | = "{" _() v:(object() ** (_() "," _())) _()(",")? _() "}" { v } 113 | 114 | rule object() -> Object 115 | = a:array() { Object::Dict(a) } / 116 | p:pair() { Object::Pair(p) } / 117 | s:string_literal() { Object::Str(s) } / 118 | z:zstr() { Object::ZStr(z) } / 119 | n:int() { Object::Int(n) } 120 | 121 | rule pair() -> Pair 122 | = key:identifier() _() "=" _() value:value() { Pair { key, value } } / 123 | "[" key:string_literal() "]" _() "=" _() value:value() 124 | { Pair { key, value } } 125 | 126 | rule value() -> Value 127 | = i:int() { Value::Int(i) } / 128 | b:bool() { Value::Bool(b) } / 129 | f:float() { Value::Float(f) } / 130 | s:string_literal() { Value::Str(s) } / 131 | a:array() { Value::Dict(a) } / 132 | z:zstr() { Value::ZStr(z) } 133 | 134 | rule int() -> i32 135 | = n:$("-"? ['0'..='9']+) !"." { i32::from_str(n).unwrap() } / expected!("integer") 136 | 137 | rule bool() -> bool 138 | = "true" { true } / "false" { false } 139 | 140 | rule float() -> f64 141 | = f:$("-"? ['0'..='9']+ "." ['0'..='9']+) { f64::from_str(f).unwrap() } / expected!("floating point") 142 | 143 | rule identifier() -> String 144 | = s:$(['a'..='z' | 'A'..='Z' | '0'..='9' | '_']+) { s.to_owned() } / expected!("identifier") 145 | 146 | // String escape, either literal EOL or quotes. 147 | rule escape() -> &'static str 148 | = "\\\"" { "\"" } / "\\\n" { "\n" } 149 | 150 | // String literal can be escaped. 151 | rule string_literal() -> String 152 | = "\"" s:((escape() / $(!['"'][_]))*) "\"" { s.join("") } 153 | 154 | rule zstr() -> String 155 | = "ZSTR" _() s:string_literal() { s } 156 | 157 | rule _() = quiet!{[' ' | '\r' | '\n' | '\t']*} 158 | 159 | }} 160 | 161 | #[test] 162 | fn test_parser() { 163 | const DATA: &str = include_str!("../data/test_lron"); 164 | let r = Object::from_string(DATA); 165 | 166 | assert!(r.is_ok()); 167 | let o = r.unwrap(); 168 | 169 | assert!(matches!(o, Object::Pair(_))); 170 | if let Object::Pair(ref p) = o { 171 | assert_eq!(p.key, "s"); 172 | assert!(matches!(p.value, Value::Dict(_))); 173 | 174 | if let Value::Dict(ref d) = p.value { 175 | assert_eq!(d.len(), 2); 176 | assert!(matches!(d[0], Object::Dict(_))); 177 | if let Object::Dict(ref d) = d[0] { 178 | assert_eq!(d.len(), 5); 179 | assert!(matches!(d[0], Object::Pair(_))); 180 | assert!(matches!(d[1], Object::Pair(_))); 181 | assert!(matches!(d[2], Object::Pair(_))); 182 | assert!(matches!(d[3], Object::Pair(_))); 183 | assert!(matches!(d[4], Object::Pair(_))); 184 | if let Object::Pair(ref p) = d[4] { 185 | assert_eq!(p.key, "someOther"); 186 | if let Value::Str(value) = &p.value { 187 | let r2 = Object::from_string(value); 188 | assert!(r2.is_ok()); 189 | } 190 | assert_eq!( 191 | p.value, 192 | Value::Str( 193 | "anObject = {\n\ 194 | key = \"lr\",\n\ 195 | }\n" 196 | .to_owned() 197 | ) 198 | ); 199 | } 200 | } 201 | assert!(matches!(d[1], Object::Pair(_))); 202 | if let Object::Pair(ref p) = d[1] { 203 | assert_eq!(p.key, "combine"); 204 | assert_eq!(p.value, Value::Str("intersect".to_owned())); 205 | } 206 | } 207 | } else { 208 | unreachable!(); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /doc/lrcat_format.md: -------------------------------------------------------------------------------- 1 | The Lightroom catalog is in a sqlite database: file *.lrcat. 2 | 3 | 4 | This document is for Lightroom 4 and Lightroom 6. 5 | Unless mentionned, info applies to both versions. 6 | 7 | Tables 8 | ------ 9 | 10 | Common concepts: 11 | 12 | Lots of tables contain `id_local` and `id_global`. 13 | 14 | `id_local` is an integer that is supposed to be uniquely incremented 15 | across the database. 16 | `id_global` is a UUID (string) that is supposed to be unique. 17 | 18 | Genealogy is also often found. It is a string denoting the path in the 19 | hierarchy. Each component is the `local_id`, but prefix with the 20 | length of the id. They are separated by '`/`' and starts with a '`/`'. 21 | 22 | `dateCreated`: a time stamp of some sort. Not UNIX epoch. 23 | 24 | ## Settings 25 | 26 | `Adobe_variablesTable` 27 | 28 | This table contain settings for Lightroom. Most of it is irrelevant. 29 | A few exceptions: 30 | 31 | * `Adobe_DBVersion`: 32 | `0200022` for Lightroom 2 33 | `0300025` for Lightroom 3 (currently not supported) 34 | `0400020` for Lightroom 4.4.1 35 | `0600008` for Lightroom 6.0 - 6.13 36 | * `AgLibraryKeyword_rootTagID`: the root keyword `local_id` (int as string) 37 | 38 | ## Keywords 39 | 40 | `AgLibraryKeyword`: keyword definitions. 41 | 42 | * `id_local`: local id 43 | * `id_global`: uuid 44 | * `dateCreated`: creation date timestamp 45 | * `genealogy`: the hierarchy 46 | * `includeOnExport`: whether to include on export 47 | * `includeParents`: whether to include parents (on export) 48 | * `includeSynonyms`: whether to include synonyms (on export) 49 | * (Lr6) `keywordType`: "person" is for tag that are `Faces`. 50 | * `lc_name`: the lowercase tag name 51 | * `name`: the tag name 52 | * `parent`: the parent (local_id) 53 | 54 | `AgLibraryKeywordImage`: keyword relation with images 55 | 56 | * `id_local`: local id 57 | * `image`: associated image `Adobe_images` 58 | * `tag`: associated keyword `AgLibraryKeyword` 59 | 60 | ## Folders 61 | 62 | Two kinds. Root and Folders. Root are top level folders and don't have 63 | a parent. 64 | 65 | `AgLibraryRootFolder`: root folder. 66 | 67 | * `id_local`: local id 68 | * `id_global`: uuid 69 | * `absolutePath`: Absolute path to the root folder. 70 | * `name`: name of the folder. 71 | * `relativePathFromCatalog`: may be NULL if on a different volume. 72 | 73 | `AgLibraryFolder`: folder. Attached to a root folder. 74 | 75 | * `id_local`: local id 76 | * `id_global`: uuid 77 | * `pathFromRoot`: path from the root folder. Can be `NULL`. 78 | * `rootFolder`: id of the root folder. 79 | 80 | There is always a folder with an empty `pathFromRoot` for a root 81 | folder (does this mean an AgLibraryFile is attached to only folders? - 82 | YES) 83 | 84 | ## Content 85 | 86 | Collections and folder have a "content" defined to declare what's 87 | inside. They is a per containe type table. 88 | 89 | This is done by defining key/values of properties. `owningValue` is 90 | the key. `content` is a value. 91 | 92 | common columns: 93 | 94 | * `id_local`: local id 95 | * `owningModule`: the key. 96 | * `content`: value of the property 97 | 98 | `AgFolderContent`: 99 | 100 | * `id_global`: uuid 101 | * `containingFolder`: the folder this content applies to 102 | * `name`: ???? 103 | 104 | `AgLibraryCollectionContent`: 105 | 106 | * `collection`: the collection this content applies to. 107 | 108 | ## Images 109 | 110 | `Adobe_images`: image. This doesn't represent physical files. 111 | 112 | * `id_local`: local id 113 | * `id_global`: uuid 114 | * `fileFormat`: string representing the format. 115 | Possible values: `RAW`, `JPG`, `VIDEO`, `DNG` 116 | * `pick`: (it's a float in the database) not 1 if picked, -1 if rejected, 0 if unpicked. 117 | * `rating`: rating value or NULL 118 | * `rootFile`: the id of the physical file (in `AgLibraryFile`) 119 | * `orientation`: text marking the orientation. ex. AB, DA. May be NULL 120 | for video. 121 | Mapping to Exif values 122 | * `AB` -> 1 123 | * `DA` -> 8 124 | * `BC` -> 6 125 | * `CD` -> 3 126 | * Not sure if the "mirrored" orientation follow the same scheme. 127 | * `captureTime`: date capture time (likely from Exif originally or as reajusted in Lr) 128 | * `masterImage`: id of master if this is a copy. NULL otherwise. 129 | * `copyName`: the name of the virtual copy. masterImage not NULL. 130 | 131 | `Adobe_imageProperties`: other properties for images. 132 | 133 | * `id_local`: local id 134 | * `id_global`: uuid 135 | * `image`: `id_local` in the `Adobe_images` table. 136 | * `propertiesString`: some properties in lron format. Seems to cover: 137 | - loupe focus point 138 | * `loupeFocusPoint` 139 | * `_ag_className` = `"AgPoint"` 140 | * `x` and `y`: floating point 0..1.0 141 | - crop aspect ratio: 142 | * `cropAspectH` 143 | * `cropAspectW` 144 | - default crop (floating point values) : 145 | * `defaultCropBottom` 146 | * `defaultCropLeft` 147 | * `defaultCropRight` 148 | * `defaultCropTop` 149 | 150 | `AgLibraryFile`: physical files. 151 | 152 | * `id_local`: local id 153 | * `id_global`: uuid 154 | * `baseName`: name without extension 155 | * `extension`: extension 156 | * `idx_filename`: index entry 157 | * `importHash`: hash at import time 158 | * `md5`: md5 digest 159 | * `originalFilename`: filename before renaming at import time 160 | * `sidecarExtensions`: extensions of sidecars. Comma `,` separated strings. 161 | For example it is `JPG,xmp` when RAW + JPEG with xmp sidecar. 162 | Can be empty. 163 | 164 | `Adobe_AdditionalMetadata`: extra metadata for images 165 | 166 | * `id_local`: local id 167 | * `id_global`: uuid 168 | * `additionalInfoSet` 169 | * `emeddedXmp`: XMP is stored in file (vs in sidecar) 170 | * `externalXmpIsDirty`: 0 or 1 171 | * `image`: local id in the `Adobe_images` table. 172 | * `incrementalWhiteBalance` 173 | * `internalXmpDigest`: (md5 or sha1 of XMP in DB?) 174 | * `isRawFile`: 1 if RAW, or 0 (likely using sidecar). 175 | * `lastSynchronizedHash`: (sha1?) 176 | * `lastSyncrhonizedTimestamp` 177 | * `metadataPresetID`: UUID of the metadata preset applied (?) 178 | * `metadataVersion`: seems to be 4.0 in Lr2, Lr4 and Lr6. 179 | * `monochrome`: 1 if monochrome? 180 | * `xmp`: the XMP packet text 181 | 182 | ## Collections 183 | 184 | `AgLibraryCollection` (Lr3, Lr4 and Lr6) - collections definitions 185 | 186 | * `id_local`: local id 187 | * `creationId`: "type" of collection. Some of the possible values: 188 | - `com.adobe.ag.library.group`: Group of collection 189 | - `com.adobe.ag.library.collection`: regular collection 190 | - `com.adobe.ag.library.smart_collection`: user collection 191 | - `com.adobe.ag.webGallery`: web gallery 192 | - `com.adobe.ag.print.unsaved`: last print (internal state) 193 | - `com.adobe.ag.webGallery.unsaved`: last web gallery (not saved) 194 | - `com.adboe.ag.slidshow.unsaved`: last slideshow (not saved) 195 | * `genealogy`: the hierarchy 196 | * `imageCount`: ???? (NULL) 197 | * `name`: String name of the collection 198 | * `parent`: NULL is id_local of parent 199 | * `systemOnly`: (seems to apply to the quick collection and *.unsaved) 200 | 201 | `AgLibraryCollectionImage` (Lr3, Lr4 and Lr6) - image to collection relation 202 | 203 | * `id_local`: local id 204 | * `collection`: local id of the collection 205 | * `image`: local id of the image 206 | * `pick` 207 | * `positionInCollection` 208 | 209 | `AgLibraryCollectionCoverImage` (Lr6) - The cover image for collections 210 | 211 | * `collection`: local id for `AgLibraryCollection` 212 | * `collectionImage`: local id for `AgLibraryCollectionImage` 213 | 214 | ### Lr2 215 | 216 | In Lr2, there is no collection table. Instead it seems to use a 217 | catch-all `AgLibraryTag` table, that include collections and other 218 | attributes. 219 | 220 | Collections are selected with `kindName == "AgCollectionTagKind"`. 221 | The quick collection is `kindName == "AgQuickCollectionTagKind"`. 222 | 223 | `AgLibraryTag` (Lr2) - Tag definitions, 224 | 225 | * `id_local`: local id 226 | * `id_glboal`: global UUID 227 | * `dateCreated`: 228 | * `genealogy`: the hierarchy 229 | * `imageCountCache`: cache of count. 230 | * `lc_name`: Normalized name 231 | * `name`: Display name 232 | * `parent`: local id of parent. Reflected in genealogy. 233 | * `kindName`: (string) 234 | * `AgCollectionTagKind`: collection 235 | * `AgQuickCollectionTagKind`: the quick collection 236 | * `AgImportTagKind`: import reference 237 | * `AgCopyrightTagKind` 238 | * `AgCaptionTagKind` 239 | * `AgMissingFileTagKind` 240 | * `AgEntireLibraryContentOwnerTagKind` 241 | * `AgLastCatalogExportTagKind` 242 | * `AgPreviousImportContentOwnerTagKind` 243 | * `AgTempImagesTagKind` 244 | * `AgUpdatedPhotoTagKind` 245 | * `sortDirection`: (NULL?) 246 | * `sordOrder`: (NULL?) 247 | 248 | `AgLibraryTagImage` (Lr2) - Tag mapping to images. 249 | 250 | * `id_local`: local id of relation 251 | * `image`: local id of image (table `Adobe_images` 252 | * `tag`: local id of tag 253 | * `tagKind`: kind of tag. Probably related to `AgLibraryTag.kindName`. 254 | 255 | ## Faces (Lr 6 only) 256 | 257 | `AgLibraryFace` - Define face detected. 258 | 259 | * `id_local`: local id 260 | * `cluster`: id of the cluster `AgLibraryFaceCluster` 261 | * `image`: id of the image 262 | 263 | `AgLibraryFaceCluster` - Group faces together 264 | 265 | * `id_local`: local id of the cluster. 266 | * `keyFace`: (NULL?) 267 | 268 | `AgLibraryFaceData` - Data of each face 269 | 270 | * `id_local`: local id 271 | * `face`: local id of the face. 272 | * `data`: blob. Apparently JP2. 273 | 274 | `AgLibraryKeywordFace` - Face to keyword equivalence 275 | 276 | * `id_local`: local id 277 | * `face`: local id of the face. 278 | * `tag`: local id of the keyword tag. 279 | * `userPick`: user said yes. 280 | * `userReject`: (rejected by user?) 281 | * `keyFace`: (NULL?) 282 | * `rankOrder`: ? 283 | -------------------------------------------------------------------------------- /src/catalog.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use std::collections::BTreeMap; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use rusqlite::{params, Connection}; 11 | 12 | use crate::collections::Collection; 13 | use crate::folders::{Folder, Folders, RootFolder}; 14 | use crate::fromdb::FromDb; 15 | use crate::images::Image; 16 | use crate::keywords::Keyword; 17 | use crate::keywordtree::KeywordTree; 18 | use crate::libraryfiles::LibraryFile; 19 | use crate::lrobject::{LrId, LrObject}; 20 | 21 | const LR2_VERSION: i32 = 2; 22 | const LR3_VERSION: i32 = 3; 23 | const LR4_VERSION: i32 = 4; 24 | const LR6_VERSION: i32 = 6; 25 | 26 | /// Catalog version. 27 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 28 | pub enum CatalogVersion { 29 | /// Unknown version 30 | Unknown, 31 | /// Lightroom 2.x catalog. 32 | Lr2, 33 | /// Lightroom 3.x catalog. Unhandled. 34 | Lr3, 35 | /// Lightroom 4.x catalog. 36 | Lr4, 37 | /// Lightroom 6.x catalog. 38 | Lr6, 39 | } 40 | 41 | impl CatalogVersion { 42 | /// Return if we support this catalog version 43 | pub fn is_supported(&self) -> bool { 44 | (*self == Self::Lr2) || (*self == Self::Lr3) || (*self == Self::Lr4) || (*self == Self::Lr6) 45 | } 46 | } 47 | 48 | /// Catalog is the main container for Lightroom. It represents 49 | /// the .lrcat database. 50 | pub struct Catalog { 51 | /// Catalog path 52 | path: PathBuf, 53 | /// The version string 54 | pub version: String, 55 | /// The catalog version 56 | pub catalog_version: CatalogVersion, 57 | /// Id for the root (top level) keyword 58 | pub root_keyword_id: LrId, 59 | 60 | /// The keywords, mapped in the local `LrId` 61 | keywords: BTreeMap, 62 | /// The `Folders` container. 63 | folders: Folders, 64 | /// The `Image` container 65 | images: Vec, 66 | /// The `LibraryFile` container 67 | libfiles: Vec, 68 | /// The `Collection` container 69 | collections: Vec, 70 | 71 | /// The sqlite connectio to the catalog 72 | dbconn: Option, 73 | } 74 | 75 | impl Catalog { 76 | /// Create a new catalog. 77 | pub fn new

(path: P) -> Catalog 78 | where 79 | P: AsRef, 80 | { 81 | Catalog { 82 | path: path.as_ref().to_path_buf(), 83 | version: "".to_string(), 84 | catalog_version: CatalogVersion::Unknown, 85 | root_keyword_id: 0, 86 | keywords: BTreeMap::new(), 87 | folders: Folders::new(), 88 | images: vec![], 89 | libfiles: vec![], 90 | collections: vec![], 91 | dbconn: None, 92 | } 93 | } 94 | 95 | /// Open catalog. Return false in failure. 96 | /// This doesn't check if the content is valid beyond the backing sqlite3. 97 | pub fn open(&mut self) -> crate::Result<()> { 98 | let conn = Connection::open(&self.path)?; 99 | 100 | self.dbconn = Some(conn); 101 | 102 | Ok(()) 103 | } 104 | 105 | /// Get a variable from the table. 106 | fn get_variable(&self, name: &str) -> Option 107 | where 108 | T: rusqlite::types::FromSql, 109 | { 110 | let conn = self.dbconn.as_ref()?; 111 | if let Ok(mut stmt) = conn.prepare("SELECT value FROM Adobe_variablesTable WHERE name=?1") { 112 | let mut rows = stmt.query([&name]).unwrap(); 113 | if let Ok(Some(row)) = rows.next() { 114 | return row.get(0).ok(); 115 | } 116 | } 117 | None 118 | } 119 | 120 | /// Parse the version string from the database. 121 | fn parse_version(mut v: String) -> i32 { 122 | v.truncate(2); 123 | v.parse::().unwrap_or_default() 124 | } 125 | 126 | /// Load version info for the catalog. 127 | pub fn load_version(&mut self) { 128 | if let Some(version) = self.get_variable::("Adobe_DBVersion") { 129 | self.version = version; 130 | let v = Catalog::parse_version(self.version.clone()); 131 | self.catalog_version = match v { 132 | LR6_VERSION => CatalogVersion::Lr6, 133 | LR4_VERSION => CatalogVersion::Lr4, 134 | LR3_VERSION => CatalogVersion::Lr3, 135 | LR2_VERSION => CatalogVersion::Lr2, 136 | _ => CatalogVersion::Unknown, 137 | }; 138 | } 139 | 140 | if let Some(root_keyword_id) = self.get_variable::("AgLibraryKeyword_rootTagID") { 141 | self.root_keyword_id = root_keyword_id.round() as LrId; 142 | } 143 | } 144 | 145 | /// Generic object loader leveraging the FromDb protocol 146 | fn load_objects(conn: &Connection, catalog_version: CatalogVersion) -> Vec { 147 | let mut query = format!( 148 | "SELECT {} FROM {}", 149 | T::read_db_columns(catalog_version), 150 | T::read_db_tables(catalog_version) 151 | ); 152 | let where_join = T::read_join_where(catalog_version); 153 | if !where_join.is_empty() { 154 | query += &format!(" WHERE {where_join}"); 155 | } 156 | if let Ok(mut stmt) = conn.prepare(&query) { 157 | if let Ok(rows) = 158 | stmt.query_and_then(params![], |row| T::read_from(catalog_version, row)) 159 | { 160 | return rows.into_iter().filter_map(|obj| obj.ok()).collect(); 161 | } 162 | } 163 | vec![] 164 | } 165 | 166 | /// Load a keyword tree 167 | pub fn load_keywords_tree(&mut self) -> KeywordTree { 168 | let keywords = self.load_keywords(); 169 | 170 | let mut tree = KeywordTree::new(); 171 | tree.add_children(keywords); 172 | 173 | tree 174 | } 175 | 176 | /// Load keywords. 177 | pub fn load_keywords(&mut self) -> &BTreeMap { 178 | if self.keywords.is_empty() { 179 | if let Some(ref conn) = self.dbconn { 180 | let result = Catalog::load_objects::(conn, self.catalog_version); 181 | for keyword in result { 182 | self.keywords.insert(keyword.id(), keyword); 183 | } 184 | } 185 | } 186 | &self.keywords 187 | } 188 | 189 | /// Get the keywords. This assume the keywords have been loaded first. 190 | /// This allow non-mutable borrowing that would be caused by `load_keywords()`. 191 | pub fn keywords(&self) -> &BTreeMap { 192 | &self.keywords 193 | } 194 | 195 | /// Load folders. 196 | pub fn load_folders(&mut self) -> &Folders { 197 | if self.folders.is_empty() { 198 | if let Some(ref conn) = self.dbconn { 199 | let folders = Catalog::load_objects::(conn, self.catalog_version); 200 | self.folders.append_root_folders(folders); 201 | let mut folders = Catalog::load_objects::(conn, self.catalog_version); 202 | for folder in &mut folders { 203 | folder.content = Some(folder.read_content(conn)); 204 | } 205 | self.folders.append_folders(folders); 206 | } 207 | } 208 | &self.folders 209 | } 210 | 211 | /// Get the folders. This assume the folders have been loaded first. 212 | /// This allow non-mutable borrowing that would be caused by `load_folders()`. 213 | pub fn folders(&self) -> &Folders { 214 | &self.folders 215 | } 216 | 217 | /// Load library files (that back images) 218 | pub fn load_library_files(&mut self) -> &Vec { 219 | if self.libfiles.is_empty() { 220 | if let Some(ref conn) = self.dbconn { 221 | let mut result = Catalog::load_objects::(conn, self.catalog_version); 222 | self.libfiles.append(&mut result); 223 | } 224 | } 225 | &self.libfiles 226 | } 227 | 228 | /// Get the libfiles. This assume the libfiles have been loaded first. 229 | /// This allow non-mutable borrowing that would be caused by `load_libfiles()`. 230 | pub fn libfiles(&self) -> &Vec { 231 | &self.libfiles 232 | } 233 | 234 | /// Load images. 235 | pub fn load_images(&mut self) -> &Vec { 236 | if self.images.is_empty() { 237 | if let Some(ref conn) = self.dbconn { 238 | let mut result = Catalog::load_objects::(conn, self.catalog_version); 239 | self.images.append(&mut result); 240 | } 241 | } 242 | &self.images 243 | } 244 | 245 | /// Get the images. This assume the images have been loaded first. 246 | /// This allow non-mutable borrowing that would be caused by `load_images()`. 247 | pub fn images(&self) -> &Vec { 248 | &self.images 249 | } 250 | 251 | /// Load collectons. 252 | pub fn load_collections(&mut self) -> &Vec { 253 | if self.collections.is_empty() { 254 | if let Some(ref conn) = self.dbconn { 255 | let mut collections = 256 | Catalog::load_objects::(conn, self.catalog_version); 257 | for collection in &mut collections { 258 | collection.content = Some(collection.read_content(conn)); 259 | } 260 | self.collections.append(&mut collections); 261 | } 262 | } 263 | &self.collections 264 | } 265 | 266 | /// Get the collections. This assume the collections have been loaded first. 267 | /// This allow non-mutable borrowing that would be caused by `load_collections()`. 268 | pub fn collections(&self) -> &Vec { 269 | &self.collections 270 | } 271 | 272 | /// Lr2 use "Tags". 273 | const LR2_QUERY: &'static str = 274 | "SELECT image FROM AgLibraryTagImage WHERE tag = ?1 AND tagKind = \"AgCollectionTagKind\""; 275 | /// Lr3, Lr4 and Lr6 store the relation in `AgLibraryCollectionImage` 276 | const LR4_QUERY: &'static str = 277 | "SELECT image FROM AgLibraryCollectionImage WHERE collection = ?1"; 278 | 279 | /// Collect images from collections using a specific query. 280 | fn images_for_collection_with_query( 281 | &self, 282 | query: &str, 283 | collection_id: LrId, 284 | ) -> super::Result> { 285 | let conn = self.dbconn.as_ref().unwrap(); 286 | let mut stmt = conn.prepare(query)?; 287 | let rows = stmt.query_map([&collection_id], |row| row.get::(0))?; 288 | let mut ids = Vec::new(); 289 | for id in rows { 290 | ids.push(id?); 291 | } 292 | Ok(ids) 293 | } 294 | 295 | /// Return the of images in the given collection. 296 | /// Not to be confused with Content. 297 | pub fn images_for_collection(&self, collection_id: LrId) -> super::Result> { 298 | match self.catalog_version { 299 | CatalogVersion::Lr2 => { 300 | self.images_for_collection_with_query(Self::LR2_QUERY, collection_id) 301 | } 302 | CatalogVersion::Lr3 | CatalogVersion::Lr4 | CatalogVersion::Lr6 => { 303 | self.images_for_collection_with_query(Self::LR4_QUERY, collection_id) 304 | } 305 | _ => Err(super::Error::UnsupportedVersion), 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/images.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | use rusqlite::Row; 8 | 9 | use crate::catalog::CatalogVersion; 10 | use crate::fromdb::FromDb; 11 | use crate::lrobject::{LrId, LrObject}; 12 | use crate::lron; 13 | use crate::{AspectRatio, Point, Rect}; 14 | 15 | /// Some misc properties of the image specific to Lr 16 | #[derive(Default, Debug)] 17 | pub struct Properties { 18 | /// Where the loupe is focused 19 | loupe_focus: Option, 20 | /// Aspect ration of the crop 21 | crop_aspect_ratio: Option, 22 | /// Default crop 23 | default_crop: Option, 24 | } 25 | 26 | impl Properties { 27 | #[allow(clippy::unnecessary_unwrap)] 28 | fn loupe_focus(value: &[lron::Object]) -> Option { 29 | use crate::lron::{Object, Value}; 30 | 31 | let mut x: Option = None; 32 | let mut y: Option = None; 33 | let mut is_point = false; 34 | value.iter().for_each(|o| { 35 | if let Object::Pair(p) = o { 36 | match p.key.as_str() { 37 | "_ag_className" => is_point = p.value == Value::Str("AgPoint".to_owned()), 38 | "y" => y = p.value.to_number(), 39 | "x" => x = p.value.to_number(), 40 | _ => {} 41 | } 42 | } 43 | }); 44 | // This triggers clippy::unnecessary_unwrap 45 | if is_point && x.is_some() && y.is_some() { 46 | Some(Point { 47 | x: x.unwrap(), 48 | y: y.unwrap(), 49 | }) 50 | } else { 51 | None 52 | } 53 | } 54 | 55 | #[allow(clippy::unnecessary_unwrap)] 56 | fn properties_from(value: &[lron::Object]) -> Self { 57 | use crate::lron::{Object, Value}; 58 | 59 | let mut props = Properties::default(); 60 | let mut crop_aspect_h: Option = None; 61 | let mut crop_aspect_w: Option = None; 62 | 63 | let mut top: Option = None; 64 | let mut bottom: Option = None; 65 | let mut left: Option = None; 66 | let mut right: Option = None; 67 | value.iter().for_each(|o| { 68 | if let Object::Pair(p) = o { 69 | match p.key.as_str() { 70 | "loupeFocusPoint" => { 71 | if let Value::Dict(ref v) = p.value { 72 | props.loupe_focus = Self::loupe_focus(v); 73 | } 74 | } 75 | "cropAspectH" => { 76 | if let Value::Int(i) = p.value { 77 | crop_aspect_h = Some(i); 78 | } 79 | } 80 | "cropAspectW" => { 81 | if let Value::Int(i) = p.value { 82 | crop_aspect_w = Some(i); 83 | } 84 | } 85 | "defaultCropBottom" => { 86 | bottom = p.value.to_number(); 87 | } 88 | "defaultCropLeft" => { 89 | left = p.value.to_number(); 90 | } 91 | "defaultCropRight" => { 92 | right = p.value.to_number(); 93 | } 94 | "defaultCropTop" => { 95 | top = p.value.to_number(); 96 | } 97 | _ => {} 98 | } 99 | } 100 | }); 101 | 102 | // This triggers clippy::unnecessary_unwrap 103 | if crop_aspect_h.is_some() && crop_aspect_w.is_some() { 104 | props.crop_aspect_ratio = Some(AspectRatio { 105 | width: crop_aspect_w.unwrap(), 106 | height: crop_aspect_h.unwrap(), 107 | }); 108 | } 109 | // This triggers clippy::unnecessary_unwrap 110 | if top.is_some() && bottom.is_some() && left.is_some() && right.is_some() { 111 | props.default_crop = Some(Rect { 112 | top: top.unwrap(), 113 | bottom: bottom.unwrap(), 114 | left: left.unwrap(), 115 | right: right.unwrap(), 116 | }); 117 | } 118 | props 119 | } 120 | } 121 | 122 | impl From for Properties { 123 | fn from(object: lron::Object) -> Self { 124 | use crate::lron::{Object, Value}; 125 | 126 | match object { 127 | Object::Pair(ref s) => { 128 | if &s.key == "properties" { 129 | match s.value { 130 | Value::Dict(ref dict) => Self::properties_from(dict), 131 | _ => Properties::default(), 132 | } 133 | } else { 134 | Properties::default() 135 | } 136 | } 137 | _ => Properties::default(), 138 | } 139 | } 140 | } 141 | 142 | /// An image in the `Catalog`. Requires a `LibraryFile` backing it 143 | pub struct Image { 144 | id: LrId, 145 | uuid: String, 146 | /// If this a copy, id of the `Image` it is a copy of 147 | pub master_image: Option, 148 | /// Name of copy. 149 | pub copy_name: Option, 150 | /// Star rating 151 | pub rating: Option, 152 | /// Backing `LibraryFile` id. 153 | pub root_file: LrId, 154 | /// File format 155 | pub file_format: String, 156 | /// Pick. -1, 0, 1 157 | pub pick: i64, 158 | /// Orientation string (set Lr format documentation) 159 | /// Convert to EXIF orientation with `self.exif_orientation()`. 160 | pub orientation: Option, 161 | /// Capture date. 162 | pub capture_time: String, 163 | /// XMP block as stored in the database. If len() == 0, 164 | /// there is no XMP. 165 | pub xmp: String, 166 | /// XMP is embedded: whether the XMP packet in the file 167 | /// like a JPEG, or in a sidecar like in a RAW (non DNG) 168 | /// file, regardless of `xmp`. 169 | pub xmp_embedded: bool, 170 | /// The external XMP (ie not in the database) is different. 171 | pub xmp_external_dirty: bool, 172 | /// Misc properties from the Adobe_imageProperties table 173 | pub properties: Option, 174 | } 175 | 176 | impl Image { 177 | /// Return the Exif value for the image orientation 178 | /// No orientation = 0. 179 | /// Error = -1 or unknown value 180 | /// Otherwise the Exif value for `orientation` 181 | pub fn exif_orientation(&self) -> i32 { 182 | self.orientation.as_ref().map_or(0, |s| match s.as_ref() { 183 | "AB" => 1, 184 | "DA" => 8, 185 | "BC" => 6, 186 | "CD" => 3, 187 | _ => -1, 188 | }) 189 | } 190 | } 191 | 192 | impl LrObject for Image { 193 | fn id(&self) -> LrId { 194 | self.id 195 | } 196 | fn uuid(&self) -> &str { 197 | &self.uuid 198 | } 199 | } 200 | 201 | impl FromDb for Image { 202 | fn read_from(_version: CatalogVersion, row: &Row) -> crate::Result { 203 | let properties = row 204 | .get::(13) 205 | .ok() 206 | .and_then(|v| lron::Object::from_string(&v).ok()) 207 | .map(Properties::from); 208 | Ok(Image { 209 | id: row.get(0)?, 210 | uuid: row.get(1)?, 211 | master_image: row.get(2).ok(), 212 | rating: row.get(3).ok(), 213 | root_file: row.get(4)?, 214 | file_format: row.get(5)?, 215 | pick: row.get(6)?, 216 | orientation: row.get(7).ok(), 217 | capture_time: row.get(8)?, 218 | copy_name: row.get(9).ok(), 219 | xmp: row.get(10)?, 220 | xmp_embedded: row.get(11)?, 221 | xmp_external_dirty: row.get(12)?, 222 | properties, 223 | }) 224 | } 225 | fn read_db_tables(_version: CatalogVersion) -> &'static str { 226 | "Adobe_images as img,Adobe_AdditionalMetadata as meta,Adobe_imageProperties as props" 227 | } 228 | fn read_db_columns(_version: CatalogVersion) -> &'static str { 229 | "img.id_local,img.id_global,img.masterImage,img.rating,img.rootFile,img.fileFormat,cast(img.pick as integer) as pick,img.orientation,img.captureTime,img.copyName,meta.xmp,meta.embeddedXmp,meta.externalXmpIsDirty,props.propertiesString" 230 | } 231 | fn read_join_where(_version: CatalogVersion) -> &'static str { 232 | "meta.image = img.id_local and props.image = img.id_local" 233 | } 234 | } 235 | 236 | #[cfg(test)] 237 | mod tests { 238 | use super::Image; 239 | use super::Properties; 240 | use crate::lron; 241 | 242 | #[test] 243 | fn test_exif_orientation() { 244 | let mut image = Image { 245 | id: 1, 246 | uuid: String::new(), 247 | master_image: None, 248 | rating: None, 249 | root_file: 2, 250 | file_format: String::from("RAW"), 251 | pick: 0, 252 | orientation: None, 253 | capture_time: String::new(), 254 | copy_name: None, 255 | xmp: String::new(), 256 | xmp_embedded: false, 257 | xmp_external_dirty: false, 258 | properties: None, 259 | }; 260 | 261 | assert_eq!(image.exif_orientation(), 0); 262 | image.orientation = Some(String::from("ZZ")); 263 | assert_eq!(image.exif_orientation(), -1); 264 | 265 | image.orientation = Some(String::from("AB")); 266 | assert_eq!(image.exif_orientation(), 1); 267 | image.orientation = Some(String::from("DA")); 268 | assert_eq!(image.exif_orientation(), 8); 269 | image.orientation = Some(String::from("BC")); 270 | assert_eq!(image.exif_orientation(), 6); 271 | image.orientation = Some(String::from("CD")); 272 | assert_eq!(image.exif_orientation(), 3); 273 | } 274 | 275 | #[test] 276 | fn test_properties_loading() { 277 | const LRON1: &str = "properties = { \ 278 | cropAspectH = 9, \ 279 | cropAspectW = 16, \ 280 | defaultCropBottom = 0.92105263157895, \ 281 | defaultCropLeft = 0, \ 282 | defaultCropRight = 1, \ 283 | defaultCropTop = 0.078947368421053, \ 284 | loupeFocusPoint = { \ 285 | _ag_className = \"AgPoint\", \ 286 | x = 0.6377015605549, \ 287 | y = 0.70538265910057, \ 288 | }, \ 289 | }"; 290 | 291 | let object = lron::Object::from_string(LRON1); 292 | 293 | assert!(object.is_ok()); 294 | let object = object.unwrap(); 295 | let properties = Properties::from(object); 296 | 297 | assert!(properties.loupe_focus.is_some()); 298 | if let Some(ref loupe_focus) = properties.loupe_focus { 299 | assert_eq!(loupe_focus.x, 0.6377015605549); 300 | assert_eq!(loupe_focus.y, 0.70538265910057); 301 | } 302 | 303 | assert!(properties.crop_aspect_ratio.is_some()); 304 | if let Some(ref ar) = properties.crop_aspect_ratio { 305 | assert_eq!(ar.height, 9); 306 | assert_eq!(ar.width, 16); 307 | } 308 | 309 | assert!(properties.default_crop.is_some()); 310 | if let Some(ref crop) = properties.default_crop { 311 | assert_eq!(crop.top, 0.078947368421053); 312 | assert_eq!(crop.bottom, 0.92105263157895); 313 | assert_eq!(crop.left, 0.0); 314 | assert_eq!(crop.right, 1.0); 315 | } 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/bin/dumper.rs: -------------------------------------------------------------------------------- 1 | /* 2 | This Source Code Form is subject to the terms of the Mozilla Public 3 | License, v. 2.0. If a copy of the MPL was not distributed with this 4 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 | */ 6 | 7 | extern crate lrcat; 8 | 9 | use std::collections::BTreeMap; 10 | use std::iter::FromIterator; 11 | use std::path::PathBuf; 12 | 13 | use clap::{Parser, Subcommand}; 14 | 15 | use lrcat::{ 16 | Catalog, Collection, Folders, Image, Keyword, KeywordTree, LibraryFile, LrId, LrObject, 17 | }; 18 | 19 | #[derive(Debug, Parser)] 20 | #[command(version)] 21 | struct Args { 22 | #[command(subcommand)] 23 | command: Command, 24 | } 25 | 26 | #[derive(Debug, Subcommand)] 27 | enum Command { 28 | /// List content of the catalog. 29 | List(ListArgs), 30 | /// Dump the catalog. 31 | Dump(CommandArgs), 32 | /// Audit (unimplemented). 33 | Audit(CommandArgs), 34 | } 35 | 36 | #[derive(Debug, Parser)] 37 | struct CommandArgs { 38 | /// Path to the catalog. 39 | path: PathBuf, 40 | /// Dump everything. 41 | #[arg(long)] 42 | all: bool, 43 | /// Dump collections. 44 | #[arg(long)] 45 | collections: bool, 46 | /// Dump library files. 47 | #[arg(long)] 48 | libfiles: bool, 49 | /// Dump images. 50 | #[arg(long)] 51 | images: bool, 52 | /// Dump folders. 53 | #[arg(long)] 54 | folders: bool, 55 | /// Dump root folders. 56 | #[arg(long)] 57 | root: bool, 58 | /// Dump keywords. 59 | #[arg(long)] 60 | keywords: bool, 61 | } 62 | 63 | #[derive(Debug, Parser)] 64 | struct ListArgs { 65 | /// The catalog 66 | path: PathBuf, 67 | /// Sort 68 | #[arg(short)] 69 | sort: bool, 70 | /// Only list directories 71 | #[arg(short)] 72 | dirs: bool, 73 | } 74 | 75 | fn main() -> lrcat::Result<()> { 76 | let args = Args::parse(); 77 | 78 | match args.command { 79 | Command::List(ref args) => process_list(args), 80 | Command::Dump(_) => process_dump(&args), 81 | Command::Audit(_) => process_audit(&args), 82 | } 83 | } 84 | 85 | fn list_dirs(folders: &BTreeMap, sort: bool) { 86 | let mut folders = folders.values().collect::>(); 87 | if sort { 88 | folders.sort_unstable(); 89 | } 90 | folders.iter().for_each(|folder| println!("{folder}")); 91 | } 92 | 93 | fn list_files(catalog: &mut Catalog, folders: &BTreeMap, sort: bool) { 94 | let libfiles = catalog.load_library_files(); 95 | let mut files = libfiles 96 | .iter() 97 | .filter_map(|file| { 98 | folders.get(&file.folder).map(|folder| { 99 | let mut out = vec![format!("{folder}{}.{}", file.basename, file.extension)]; 100 | out.extend(file.sidecar_extensions.split(',').filter_map(|ext| { 101 | if !ext.is_empty() { 102 | Some(format!("{folder}{}.{ext}", file.basename)) 103 | } else { 104 | None 105 | } 106 | })); 107 | 108 | out 109 | }) 110 | }) 111 | .flatten() 112 | .collect::>(); 113 | 114 | if sort { 115 | files.sort_unstable(); 116 | } 117 | files.iter().for_each(|file| println!("{file}")); 118 | } 119 | 120 | fn process_list(args: &ListArgs) -> lrcat::Result<()> { 121 | let mut catalog = Catalog::new(&args.path); 122 | catalog.open()?; 123 | let folders = catalog.load_folders(); 124 | 125 | let roots = BTreeMap::from_iter( 126 | folders 127 | .roots 128 | .iter() 129 | .map(|folder| (folder.id(), folder.clone())), 130 | ); 131 | 132 | let resolved_folders = BTreeMap::from_iter(folders.folders.iter().map(|folder| { 133 | let root_path = if let Some(root) = roots.get(&folder.root_folder) { 134 | &root.absolute_path 135 | } else { 136 | "" 137 | }; 138 | 139 | ( 140 | folder.id(), 141 | format!("{}{}", root_path, &folder.path_from_root), 142 | ) 143 | })); 144 | 145 | if args.dirs { 146 | list_dirs(&resolved_folders, args.sort); 147 | } else { 148 | list_files(&mut catalog, &resolved_folders, args.sort); 149 | } 150 | 151 | Ok(()) 152 | } 153 | 154 | fn process_dump(args: &Args) -> lrcat::Result<()> { 155 | if let Command::Dump(args) = &args.command { 156 | let mut catalog = Catalog::new(&args.path); 157 | catalog.open()?; 158 | 159 | catalog.load_version(); 160 | println!("Catalog:"); 161 | println!( 162 | "\tVersion: {} ({:?})", 163 | catalog.version, catalog.catalog_version 164 | ); 165 | println!("\tRoot keyword id: {}", catalog.root_keyword_id); 166 | 167 | if !catalog.catalog_version.is_supported() { 168 | println!("Unsupported catalog version"); 169 | return Err(lrcat::Error::UnsupportedVersion); 170 | } 171 | 172 | { 173 | let root_keyword_id = catalog.root_keyword_id; 174 | let keywordtree = catalog.load_keywords_tree(); 175 | let keywords = catalog.load_keywords(); 176 | println!("\tKeywords count: {}", keywords.len()); 177 | 178 | if args.all || args.keywords { 179 | dump_keywords(root_keyword_id, keywords, &keywordtree); 180 | } 181 | } 182 | 183 | { 184 | let folders = catalog.load_folders(); 185 | if args.all || args.root { 186 | dump_root_folders(folders); 187 | } 188 | if args.all || args.folders { 189 | dump_folders(folders); 190 | } 191 | } 192 | 193 | { 194 | let libfiles = catalog.load_library_files(); 195 | if args.all || args.libfiles { 196 | dump_libfiles(libfiles); 197 | } 198 | } 199 | { 200 | let images = catalog.load_images(); 201 | if args.all || args.images { 202 | dump_images(images); 203 | } 204 | } 205 | { 206 | let collections = catalog.load_collections(); 207 | if args.all || args.collections { 208 | dump_collections(collections); 209 | } 210 | } 211 | } 212 | Ok(()) 213 | } 214 | 215 | fn print_keyword(level: i32, id: LrId, keywords: &BTreeMap, tree: &KeywordTree) { 216 | if let Some(keyword) = keywords.get(&id) { 217 | let mut indent = String::from(""); 218 | if level > 0 { 219 | for _ in 0..level - 1 { 220 | indent.push(' '); 221 | } 222 | indent.push_str("+ ") 223 | } 224 | println!( 225 | "| {:>7} | {} | {:>7} | {}{}", 226 | keyword.id(), 227 | keyword.uuid(), 228 | keyword.parent, 229 | indent, 230 | keyword.name 231 | ); 232 | let children = tree.children_for(id); 233 | for child in children { 234 | print_keyword(level + 1, child, keywords, tree); 235 | } 236 | } 237 | } 238 | 239 | fn dump_keywords(root: LrId, keywords: &BTreeMap, tree: &KeywordTree) { 240 | println!("Keywords"); 241 | println!( 242 | "+---------+--------------------------------------+---------+----------------------------" 243 | ); 244 | println!("| id | uuid | parent | name"); 245 | println!( 246 | "+---------+--------------------------------------+---------+----------------------------" 247 | ); 248 | print_keyword(0, root, keywords, tree); 249 | println!( 250 | "+---------+--------------------------------------+---------+----------------------------" 251 | ); 252 | } 253 | 254 | fn dump_root_folders(folders: &Folders) { 255 | println!("Root Folders"); 256 | println!("+---------+--------------------------------------+------------------+----------------------------"); 257 | println!("| id | uuid | name | absolute path"); 258 | println!("+---------+--------------------------------------+------------------+----------------------------"); 259 | for root in &folders.roots { 260 | println!( 261 | "| {:>7} | {} | {:<16} | {:<26}", 262 | root.id(), 263 | root.uuid(), 264 | root.name, 265 | root.absolute_path 266 | ); 267 | } 268 | println!("+---------+--------------------------------------+------------------+----------------------------"); 269 | } 270 | 271 | fn dump_folders(folders: &Folders) { 272 | println!("+---------+--------------------------------------+------------------+----------------------------"); 273 | println!("Folders"); 274 | println!("+---------+--------------------------------------+--------+-----------------------------+----------"); 275 | println!( 276 | "| id | uuid | root | path |" 277 | ); 278 | println!("+---------+--------------------------------------+--------+-----------------------------+----------"); 279 | for folder in &folders.folders { 280 | println!( 281 | "| {:>7} | {} | {:>7} | {:<26} | {:?}", 282 | folder.id(), 283 | folder.uuid(), 284 | folder.root_folder, 285 | folder.path_from_root, 286 | folder.content 287 | ); 288 | } 289 | println!("+---------+--------------------------------------+--------+-----------------------------+----------"); 290 | } 291 | 292 | fn dump_libfiles(libfiles: &[LibraryFile]) { 293 | println!("Libfiles"); 294 | println!("+---------+--------------------------------------+---------+--------+---------------------+----------+"); 295 | println!("| id | uuid | folder | extens | basename | sidecars |"); 296 | println!("+---------+--------------------------------------+---------+--------+---------------------+----------+"); 297 | for libfile in libfiles { 298 | println!( 299 | "| {:>7} | {} | {:>7} | {:<6} | {:<19} | {:<8} |", 300 | libfile.id(), 301 | libfile.uuid(), 302 | libfile.folder, 303 | libfile.extension, 304 | libfile.basename, 305 | libfile.sidecar_extensions 306 | ); 307 | } 308 | println!("+---------+--------------------------------------+---------+--------+---------------------+----------+"); 309 | } 310 | 311 | fn dump_images(images: &[Image]) { 312 | println!("Images"); 313 | println!("+---------+--------------------------------------+---------+--------+-------+----+-----------"); 314 | println!( 315 | "| id | uuid | root | format | or | P | xmp " 316 | ); 317 | println!("+---------+--------------------------------------+---------+--------+-------+----+-----------"); 318 | for image in images { 319 | println!( 320 | "| {:>7} | {} | {:>7} | {:<6} | {:<2}({}) | {:>2} | {} bytes ", 321 | image.id(), 322 | image.uuid(), 323 | image.root_file, 324 | image.file_format, 325 | image.orientation.as_ref().unwrap_or(&String::new()), 326 | image.exif_orientation(), 327 | image.pick, 328 | image.xmp.len(), 329 | ); 330 | } 331 | println!("+---------+--------------------------------------+---------+--------+-------+----+-----------"); 332 | } 333 | 334 | fn dump_collections(collections: &[Collection]) { 335 | println!("Collections"); 336 | println!("+---------+--------------------------------------+---------+-------+----------------------"); 337 | println!("| id | name | parent | syst | content"); 338 | println!("+---------+--------------------------------------+---------+-------+----------------------"); 339 | for collection in collections { 340 | println!( 341 | "| {:>7} | {:<36} | {:>7} | {:<5} | {:?}", 342 | collection.id(), 343 | collection.name, 344 | collection.parent, 345 | collection.system_only, 346 | collection.content 347 | ) 348 | } 349 | println!("+---------+--------------------------------------+---------+-------+----------------------"); 350 | } 351 | 352 | fn process_audit(_: &Args) -> lrcat::Result<()> { 353 | Err(lrcat::Error::Unimplemented) 354 | } 355 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.19" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.11" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.3" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 55 | dependencies = [ 56 | "windows-sys", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.9" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 64 | dependencies = [ 65 | "anstyle", 66 | "once_cell_polyfill", 67 | "windows-sys", 68 | ] 69 | 70 | [[package]] 71 | name = "autocfg" 72 | version = "1.5.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 75 | 76 | [[package]] 77 | name = "bitflags" 78 | version = "2.9.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 81 | 82 | [[package]] 83 | name = "bumpalo" 84 | version = "3.19.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 87 | 88 | [[package]] 89 | name = "cc" 90 | version = "1.2.30" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" 93 | dependencies = [ 94 | "shlex", 95 | ] 96 | 97 | [[package]] 98 | name = "cfg-if" 99 | version = "1.0.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 102 | 103 | [[package]] 104 | name = "chrono" 105 | version = "0.4.41" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 108 | dependencies = [ 109 | "android-tzdata", 110 | "iana-time-zone", 111 | "js-sys", 112 | "num-traits", 113 | "wasm-bindgen", 114 | "windows-link", 115 | ] 116 | 117 | [[package]] 118 | name = "clap" 119 | version = "4.5.41" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" 122 | dependencies = [ 123 | "clap_builder", 124 | "clap_derive", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_builder" 129 | version = "4.5.41" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" 132 | dependencies = [ 133 | "anstream", 134 | "anstyle", 135 | "clap_lex", 136 | "strsim", 137 | ] 138 | 139 | [[package]] 140 | name = "clap_derive" 141 | version = "4.5.41" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" 144 | dependencies = [ 145 | "heck", 146 | "proc-macro2", 147 | "quote", 148 | "syn", 149 | ] 150 | 151 | [[package]] 152 | name = "clap_lex" 153 | version = "0.7.5" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 156 | 157 | [[package]] 158 | name = "colorchoice" 159 | version = "1.0.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 162 | 163 | [[package]] 164 | name = "core-foundation-sys" 165 | version = "0.8.7" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 168 | 169 | [[package]] 170 | name = "fallible-iterator" 171 | version = "0.3.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 174 | 175 | [[package]] 176 | name = "fallible-streaming-iterator" 177 | version = "0.1.9" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 180 | 181 | [[package]] 182 | name = "foldhash" 183 | version = "0.1.5" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 186 | 187 | [[package]] 188 | name = "hashbrown" 189 | version = "0.15.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 192 | dependencies = [ 193 | "foldhash", 194 | ] 195 | 196 | [[package]] 197 | name = "hashlink" 198 | version = "0.10.0" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 201 | dependencies = [ 202 | "hashbrown", 203 | ] 204 | 205 | [[package]] 206 | name = "heck" 207 | version = "0.5.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 210 | 211 | [[package]] 212 | name = "iana-time-zone" 213 | version = "0.1.63" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 216 | dependencies = [ 217 | "android_system_properties", 218 | "core-foundation-sys", 219 | "iana-time-zone-haiku", 220 | "js-sys", 221 | "log", 222 | "wasm-bindgen", 223 | "windows-core", 224 | ] 225 | 226 | [[package]] 227 | name = "iana-time-zone-haiku" 228 | version = "0.1.2" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 231 | dependencies = [ 232 | "cc", 233 | ] 234 | 235 | [[package]] 236 | name = "is_terminal_polyfill" 237 | version = "1.70.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 240 | 241 | [[package]] 242 | name = "js-sys" 243 | version = "0.3.77" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 246 | dependencies = [ 247 | "once_cell", 248 | "wasm-bindgen", 249 | ] 250 | 251 | [[package]] 252 | name = "libc" 253 | version = "0.2.174" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 256 | 257 | [[package]] 258 | name = "libsqlite3-sys" 259 | version = "0.35.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" 262 | dependencies = [ 263 | "pkg-config", 264 | "vcpkg", 265 | ] 266 | 267 | [[package]] 268 | name = "log" 269 | version = "0.4.27" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 272 | 273 | [[package]] 274 | name = "lrcat-extractor" 275 | version = "0.5.0" 276 | dependencies = [ 277 | "chrono", 278 | "clap", 279 | "peg", 280 | "rusqlite", 281 | "thiserror", 282 | ] 283 | 284 | [[package]] 285 | name = "num-traits" 286 | version = "0.2.19" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 289 | dependencies = [ 290 | "autocfg", 291 | ] 292 | 293 | [[package]] 294 | name = "once_cell" 295 | version = "1.21.3" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 298 | 299 | [[package]] 300 | name = "once_cell_polyfill" 301 | version = "1.70.1" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 304 | 305 | [[package]] 306 | name = "peg" 307 | version = "0.8.5" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" 310 | dependencies = [ 311 | "peg-macros", 312 | "peg-runtime", 313 | ] 314 | 315 | [[package]] 316 | name = "peg-macros" 317 | version = "0.8.5" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" 320 | dependencies = [ 321 | "peg-runtime", 322 | "proc-macro2", 323 | "quote", 324 | ] 325 | 326 | [[package]] 327 | name = "peg-runtime" 328 | version = "0.8.5" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" 331 | 332 | [[package]] 333 | name = "pkg-config" 334 | version = "0.3.32" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 337 | 338 | [[package]] 339 | name = "proc-macro2" 340 | version = "1.0.95" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 343 | dependencies = [ 344 | "unicode-ident", 345 | ] 346 | 347 | [[package]] 348 | name = "quote" 349 | version = "1.0.40" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 352 | dependencies = [ 353 | "proc-macro2", 354 | ] 355 | 356 | [[package]] 357 | name = "rusqlite" 358 | version = "0.37.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" 361 | dependencies = [ 362 | "bitflags", 363 | "fallible-iterator", 364 | "fallible-streaming-iterator", 365 | "hashlink", 366 | "libsqlite3-sys", 367 | "smallvec", 368 | ] 369 | 370 | [[package]] 371 | name = "rustversion" 372 | version = "1.0.21" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 375 | 376 | [[package]] 377 | name = "shlex" 378 | version = "1.3.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 381 | 382 | [[package]] 383 | name = "smallvec" 384 | version = "1.15.1" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 387 | 388 | [[package]] 389 | name = "strsim" 390 | version = "0.11.1" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 393 | 394 | [[package]] 395 | name = "syn" 396 | version = "2.0.104" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 399 | dependencies = [ 400 | "proc-macro2", 401 | "quote", 402 | "unicode-ident", 403 | ] 404 | 405 | [[package]] 406 | name = "thiserror" 407 | version = "2.0.12" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 410 | dependencies = [ 411 | "thiserror-impl", 412 | ] 413 | 414 | [[package]] 415 | name = "thiserror-impl" 416 | version = "2.0.12" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 419 | dependencies = [ 420 | "proc-macro2", 421 | "quote", 422 | "syn", 423 | ] 424 | 425 | [[package]] 426 | name = "unicode-ident" 427 | version = "1.0.18" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 430 | 431 | [[package]] 432 | name = "utf8parse" 433 | version = "0.2.2" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 436 | 437 | [[package]] 438 | name = "vcpkg" 439 | version = "0.2.15" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 442 | 443 | [[package]] 444 | name = "wasm-bindgen" 445 | version = "0.2.100" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 448 | dependencies = [ 449 | "cfg-if", 450 | "once_cell", 451 | "rustversion", 452 | "wasm-bindgen-macro", 453 | ] 454 | 455 | [[package]] 456 | name = "wasm-bindgen-backend" 457 | version = "0.2.100" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 460 | dependencies = [ 461 | "bumpalo", 462 | "log", 463 | "proc-macro2", 464 | "quote", 465 | "syn", 466 | "wasm-bindgen-shared", 467 | ] 468 | 469 | [[package]] 470 | name = "wasm-bindgen-macro" 471 | version = "0.2.100" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 474 | dependencies = [ 475 | "quote", 476 | "wasm-bindgen-macro-support", 477 | ] 478 | 479 | [[package]] 480 | name = "wasm-bindgen-macro-support" 481 | version = "0.2.100" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 484 | dependencies = [ 485 | "proc-macro2", 486 | "quote", 487 | "syn", 488 | "wasm-bindgen-backend", 489 | "wasm-bindgen-shared", 490 | ] 491 | 492 | [[package]] 493 | name = "wasm-bindgen-shared" 494 | version = "0.2.100" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 497 | dependencies = [ 498 | "unicode-ident", 499 | ] 500 | 501 | [[package]] 502 | name = "windows-core" 503 | version = "0.61.2" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 506 | dependencies = [ 507 | "windows-implement", 508 | "windows-interface", 509 | "windows-link", 510 | "windows-result", 511 | "windows-strings", 512 | ] 513 | 514 | [[package]] 515 | name = "windows-implement" 516 | version = "0.60.0" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 519 | dependencies = [ 520 | "proc-macro2", 521 | "quote", 522 | "syn", 523 | ] 524 | 525 | [[package]] 526 | name = "windows-interface" 527 | version = "0.59.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 530 | dependencies = [ 531 | "proc-macro2", 532 | "quote", 533 | "syn", 534 | ] 535 | 536 | [[package]] 537 | name = "windows-link" 538 | version = "0.1.3" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 541 | 542 | [[package]] 543 | name = "windows-result" 544 | version = "0.3.4" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 547 | dependencies = [ 548 | "windows-link", 549 | ] 550 | 551 | [[package]] 552 | name = "windows-strings" 553 | version = "0.4.2" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 556 | dependencies = [ 557 | "windows-link", 558 | ] 559 | 560 | [[package]] 561 | name = "windows-sys" 562 | version = "0.59.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 565 | dependencies = [ 566 | "windows-targets", 567 | ] 568 | 569 | [[package]] 570 | name = "windows-targets" 571 | version = "0.52.6" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 574 | dependencies = [ 575 | "windows_aarch64_gnullvm", 576 | "windows_aarch64_msvc", 577 | "windows_i686_gnu", 578 | "windows_i686_gnullvm", 579 | "windows_i686_msvc", 580 | "windows_x86_64_gnu", 581 | "windows_x86_64_gnullvm", 582 | "windows_x86_64_msvc", 583 | ] 584 | 585 | [[package]] 586 | name = "windows_aarch64_gnullvm" 587 | version = "0.52.6" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 590 | 591 | [[package]] 592 | name = "windows_aarch64_msvc" 593 | version = "0.52.6" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 596 | 597 | [[package]] 598 | name = "windows_i686_gnu" 599 | version = "0.52.6" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 602 | 603 | [[package]] 604 | name = "windows_i686_gnullvm" 605 | version = "0.52.6" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 608 | 609 | [[package]] 610 | name = "windows_i686_msvc" 611 | version = "0.52.6" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 614 | 615 | [[package]] 616 | name = "windows_x86_64_gnu" 617 | version = "0.52.6" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 620 | 621 | [[package]] 622 | name = "windows_x86_64_gnullvm" 623 | version = "0.52.6" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 626 | 627 | [[package]] 628 | name = "windows_x86_64_msvc" 629 | version = "0.52.6" 630 | source = "registry+https://github.com/rust-lang/crates.io-index" 631 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 632 | --------------------------------------------------------------------------------